Skip to main content

agent_block_core/knl/
session.rs

1//! K5 — the session.
2//!
3//! A session binds one history, one budget and the projection caches
4//! together and is the only handle on kernel state.  All of it lives in
5//! the value: two sessions share nothing.
6//!
7//! # The lifecycle is the session's
8//!
9//! There is no "run" inside a session.  A session opens once, records, and
10//! closes once; the two events that bracket it — `session_opened` and
11//! `session_closed` — are written by the kernel on those two occasions and
12//! by nothing else.  A caller cannot hand-append either
13//! ([`super::event::is_kernel_only`]), because a stream that claims an
14//! opening it never had, or an ending it never reached, is exactly what a
15//! resume and an audit read.
16//!
17//! Beats are the layer above's, not the kernel's: the shell mints a beat id
18//! and stamps it on the facts that belong together.  The kernel neither
19//! numbers nor requires one — see [`super::event`].
20//!
21//! # The session *has a* scope
22//!
23//! A scope and a session are two things, sharing one lifetime: both begin
24//! when the session opens and end when it closes.  The session is the
25//! stream — this history and its projections.  The [`Scope`] is the
26//! authority it is written under: a kernel-issued [`ScopeId`], the `owner`,
27//! and the quota that owner granted.  The session holds it *by value*,
28//! because neither outlives the other and there is nothing to share.
29//!
30//! A session holds only its own events, so ownership is not a per-event
31//! question: the scope carries one `owner` — a real principal id, or the
32//! reserved [`ANON`] / [`SYSTEM`] id — and it is *total* (never `Option`,
33//! never a "kernel vs caller" flag).  System-originated work is a session
34//! whose owner is [`SYSTEM`]; unspecified is [`ANON`].  The policy layer
35//! above the kernel reads [`Session::owner`] to authorize; the kernel itself
36//! does not branch on it.
37//!
38//! The scope is in the log, not only in the value.  Its id is recorded on
39//! `session_opened` beside the owner, and on every `budget_*` event, so the
40//! boundary is recoverable — and unforgeable, since the kinds that carry it
41//! are the kernel's alone to write ([`super::event::is_kernel_only`]).
42//! [`Session::resume`] restores the scope from those records rather than
43//! being told what it was.
44//!
45//! # One append, and it lands
46//!
47//! There is a single write path.  [`Session::append`] validates, stamps
48//! the kernel-owned `seq` / `epoch_ms`, and pushes.  It adds nothing else:
49//! what an event says beyond the envelope is the caller's.
50//!
51//! An append *records a fact*, so it is never refused for what the handle
52//! last saw.  The store assigns the `seq` and the ordering, and serializes
53//! the write per stream; two handles on one stream both write and the log
54//! interleaves in arrival order.  No handle keeps a head of its own to be
55//! measured against — the store's head is read when something needs it.
56//!
57//! A *command with an invariant* is the other shape, and it is decided
58//! inside the store ([`EventStore::append_if`]), which folds the events it
59//! is handed and writes only if the invariant holds, all under the same
60//! serialization.  Checking a cached value out here and appending
61//! afterwards is exactly the race that would let two handles reserve the
62//! same allowance twice.  Every move of the balance is one:
63//! [`Session::reserve`] writes a `budget_reserved` if the ledger covers what
64//! was asked and a `budget_refused` if it does not, [`Session::spend`] writes
65//! a `budget_spent` if there is a ledger at all, and
66//! [`Session::grant_on_resume`] writes a `budget_granted` only where one
67//! already is.  What all three decide first is the same question — *does this
68//! stream have a ledger* — and it is the log's answer, not the handle's: a
69//! grant this handle never saw still binds it.  None of them asks whether the
70//! session ended — see below.
71//!
72//! The one thing written as a *batch* is the session's own opening:
73//! [`Session::open_on`] records `session_opened` and the `budget_granted`
74//! that says what it opened under through [`EventStore::append_many`], so a
75//! reader never meets a session that opened without its quota.
76//!
77//! # The log never refuses a write
78//!
79//! `closed` is the *handle's* state, not the stream's.  A handle that has
80//! closed will not operate again — [`Session::append`], [`Session::reserve`],
81//! [`Session::spend`] and [`Session::close`] all read that one local flag —
82//! but the log itself turns nothing away.  A write that arrives after a
83//! `session_closed`, from another handle that never saw the ending, is
84//! recorded like any other, because it *is* a fact: something wrote to a
85//! stream that had ended, which is exactly what an audit is there to find.
86//! Refusing it would delete the evidence of the bug that produced it.
87//!
88//! So two handles closing leave two `session_closed` events, not one, and
89//! that is the truthful record.  The store's job is to serialize appends and
90//! land them; deciding what a stream *ought* to have looked like is a
91//! reader's.
92//!
93//! # A session is disposable
94//!
95//! Nothing else looks at `session_closed`: [`Session::resume`] is its one
96//! reader.  A stream whose log already carries an ending is not continued —
97//! what comes after an ending is a new session, not a second life for the
98//! old one, or "closed" would say nothing about what a reader of the log can
99//! expect after it.  That is where a closed stream refuses; the writes do
100//! not.
101//!
102//! # A child is a fact, not a handle
103//!
104//! [`Session::open_child`] opens a session from this one and pays for it out
105//! of this one's balance.  It is the second *command with an invariant* the
106//! kernel has, and the only one whose write spans two streams
107//! ([`EventStore::append_if_many`]): the child's `session_opened` and
108//! `budget_granted` land on the child's stream in the same transaction as the
109//! `budget_reserved` on this one, or a `budget_refused` lands here and no
110//! child is opened at all.  Both streams are in one database, which is
111//! checked before anything is written — an allocation that could half-land
112//! would leave units in neither ledger.
113//!
114//! The session holds nothing afterwards.  There is no list of children here,
115//! no pointer to a parent, and no cascade: what the kernel knows about the
116//! structure is in the log — `session_opened.data.parent` on the child, and
117//! the child's stream named on the parent's ledger entry — and a supervisor
118//! reads it back with a query.  A close *records* the children that had not
119//! ended ([`Session::close`], `session_closed.data.open_children`) inside the
120//! same write as the boundary, and lands anyway: the log never refuses a
121//! write, and what to do about a subtree that outlived its root is a
122//! decision, which is not the kernel's to take.
123//!
124//! # Stored shape change ⇒ upcaster
125//!
126//! Every read a session makes — the restore fold, the view folds, `events`,
127//! the balance fold — goes through the read-time upcaster chain
128//! ([`super::event_store::kernel_upcasters`]), so a log written under an
129//! older shape reads as the current one and the stored bytes are never
130//! rewritten.  The chain is empty until the first release, because there is
131//! no released shape to read yet; from then on, a round that changes what is
132//! stored ships the matching `n → n+1` step in the same breath.  See the
133//! [`super::event_store`] module docs.
134//!
135//! That is a property of the types here, not a rule to remember: a session
136//! holds a [`CurrentStore`] and never a bare backend, so every event it hands
137//! to a fold — or out through [`Session::events`] — is a
138//! [`Current`](super::event_store::Current), and a read that skipped the
139//! chain has no way to reach one.
140//!
141//! An append does not charge.  It is a record of something that happened,
142//! and the budget is a quota: [`Session::reserve`] is a deduction that
143//! refuses when the balance is short, [`Session::spend`] is a deduction that
144//! does not ask.  They are independent — nothing is held and nothing is
145//! released, so a beat that calls both deducts twice — and the layer that
146//! knows what a call costs picks.  Folding the budget into the append is what
147//! turned it into a flag that only stands up once the allowance is already
148//! gone; the balance and what a run actually consumed (a query view over the
149//! recorded `llm_response` payloads, on the Lua side) are independent
150//! readings and neither is the other's ledger.
151//!
152//! # The budget is in the log, and nowhere else
153//!
154//! Every move of the balance is an event — `budget_granted` when an owner
155//! allows, `budget_reserved` / `budget_refused` at the decision point,
156//! `budget_spent` for a deduction that did not ask — written through the same
157//! store.  The
158//! balance is not session-local state that dies with the process, and it is
159//! not a number kept beside the log either: it *is*
160//! [`super::budget::fold_balance`] over the stream, which is why
161//! [`Session::remaining`] is right on a stream more than one handle writes
162//! to.  Reading it is cheap because the fold is cached against the store's
163//! head and retaken only when the head has moved; nothing but that fold ever
164//! sets it.  Those kinds are the kernel's alone to write
165//! ([`super::event::is_kernel_only`]) — [`Session::append`] refuses them
166//! from a caller, because writing one is moving the account.
167//!
168//! The kernel writes the session's own boundaries through the same append:
169//! [`Session::new`] appends `session_opened` and [`Session::close`] appends
170//! `session_closed`, so a session is bracketed in the history whether or not
171//! the shell remembers to say so.  After a close, *this handle's* `append` /
172//! `reserve` / `spend` are errors while reads keep working — the record
173//! outlives the session, and another handle's writes go on landing in it.
174//!
175//! What ends a session is [`close`], and only [`close`] writes the
176//! `session_closed` that says so: the flag and the event are set on one
177//! path, so a handle's state and what it wrote cannot disagree.  Taking that
178//! path twice on one handle writes once, because the flag is already set the
179//! second time.
180//!
181//! [`close`]: Session::close
182
183use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
184use std::sync::{Arc, Mutex, PoisonError};
185
186use serde_json::{Map, Value};
187
188use super::budget::{self, fold_balance, last_grant, Allocation, BudgetGrant};
189use super::event::{
190    data_field, is_kernel_only, kernel_event, BUDGET_KINDS, FIELD_AMOUNT, FIELD_CHILD, FIELD_DESC,
191    FIELD_DETAIL, FIELD_KIND, FIELD_OPEN_CHILDREN, FIELD_OWNER, FIELD_PARENT, FIELD_REASON,
192    FIELD_REMAINING, FIELD_SCOPE_ID, FIELD_TAG, KIND_BUDGET_GRANTED, KIND_BUDGET_REFUSED,
193    KIND_BUDGET_RESERVED, KIND_BUDGET_SPENT, KIND_SESSION_CLOSED, KIND_SESSION_OPENED,
194};
195use super::event_store::{ChildScan, Current, CurrentStore, EventStore, Split};
196use super::logs::Logs;
197use super::projection::{tail_count, VIEW_TAIL};
198use super::query::{self, QueryOpts, QueryParams, QueryRows};
199use super::scope::{Scope, ScopeId};
200use super::sqlite_store::SqliteEventStore;
201use super::{projection, KnlError, KnlResult};
202
203/// Reason recorded by `close()` when the caller does not give one.
204pub const DEFAULT_CLOSE_REASON: &str = "closed";
205/// Reason recorded when a session ended on its own: the Lua `<close>`
206/// variable holding the session went out of scope with no error.
207pub const CLOSE_REASON_SCOPE_EXIT: &str = "scope_exit";
208/// Reason recorded when a run scope ended because the block raised: the
209/// message goes to [`FIELD_DETAIL`], never into the reason.
210pub const CLOSE_REASON_ERROR: &str = "error";
211/// Reason recorded by the backstop: the handle died without anyone closing
212/// it, so the boundary is written where the value is dropped.
213pub const CLOSE_REASON_DROPPED: &str = "dropped";
214
215/// What a closed handle says when it is asked to write.
216///
217/// One sentence for both places it comes from — a handle that has closed,
218/// and a [`Session::resume`] of a stream whose log already ended — because
219/// from the caller's side they are the same answer: this session is over,
220/// go and open another.
221const CLOSED: &str = "session is closed";
222
223/// Whether the stream already carries its ending.
224///
225/// Asked in exactly one place, [`Session::resume`], because a session is
226/// disposable: there is no reopening kind, so a single `session_closed`
227/// anywhere in the log means the stream is not a state to continue from.
228/// No *write* asks this — a write records what happened, and something
229/// writing after an ending is the fact an audit most wants recorded.
230///
231/// It reads the one kind it is asking about, and at most one of those: the
232/// question is whether an ending exists, not where it is or how many there
233/// are.
234async fn has_ended(store: &CurrentStore) -> KnlResult<bool> {
235    let ending = store.read_kinds(Some(&[KIND_SESSION_CLOSED]), 0, 1).await?;
236    Ok(!ending.is_empty())
237}
238
239/// Reserved owner: no principal was named when the session opened.
240pub const ANON: &str = "anon";
241/// Reserved owner: the session belongs to the system itself.
242pub const SYSTEM: &str = "system";
243
244/// A `budget_granted` event for `grant`, written under `scope_id`.
245///
246/// Only what the owner said is written — an absent `tag` is an absent
247/// field, not a null — so the record carries the grant and nothing more.
248/// The fields are the kind's own, so they go under `data`
249/// ([`super::event`]); the envelope carries the log's vocabulary and not the
250/// ledger's.
251fn granted_event(grant: &BudgetGrant, scope_id: &str) -> Map<String, Value> {
252    let mut data = Map::new();
253    data.insert(
254        FIELD_SCOPE_ID.to_string(),
255        Value::from(scope_id.to_string()),
256    );
257    data.insert(FIELD_AMOUNT.to_string(), Value::from(grant.amount));
258    if let Some(tag) = grant.tag.as_ref() {
259        data.insert(FIELD_TAG.to_string(), Value::from(tag.clone()));
260    }
261    if let Some(desc) = grant.desc.as_ref() {
262        data.insert(FIELD_DESC.to_string(), Value::from(desc.clone()));
263    }
264    kernel_event(KIND_BUDGET_GRANTED, data)
265}
266
267/// A `budget_reserved` / `budget_spent` / `budget_refused` event for
268/// `amount`, written under `scope_id` and tagged with the grant's unit so
269/// the ledger reads without a join.
270///
271/// The scope id is on every move of the balance, not only on the session's
272/// opening: the ledger is the one part of the log that says what was
273/// *allowed*, so each entry names the authority it was allowed under.
274fn budget_move_event(
275    kind: &str,
276    amount: i64,
277    tag: Option<&str>,
278    scope_id: &str,
279) -> Map<String, Value> {
280    kernel_event(kind, budget_move_data(amount, tag, scope_id))
281}
282
283/// The `data` every `budget_*` entry carries: the scope it was allowed
284/// under, how much, and the grant's unit if it named one.
285fn budget_move_data(amount: i64, tag: Option<&str>, scope_id: &str) -> Map<String, Value> {
286    let mut data = Map::new();
287    data.insert(
288        FIELD_SCOPE_ID.to_string(),
289        Value::from(scope_id.to_string()),
290    );
291    data.insert(FIELD_AMOUNT.to_string(), Value::from(amount));
292    if let Some(tag) = tag {
293        data.insert(FIELD_TAG.to_string(), Value::from(tag.to_string()));
294    }
295    data
296}
297
298/// A `budget_refused` event: a move that did not happen, carrying what was
299/// asked for *and* the balance it was measured against.
300///
301/// The pair is what makes a refusal readable without folding the ledger, so
302/// `remaining` is built into the entry rather than added to it afterwards.
303fn refused_event(
304    amount: i64,
305    remaining: i64,
306    tag: Option<&str>,
307    scope_id: &str,
308) -> Map<String, Value> {
309    let mut data = budget_move_data(amount, tag, scope_id);
310    data.insert(FIELD_REMAINING.to_string(), Value::from(remaining));
311    kernel_event(KIND_BUDGET_REFUSED, data)
312}
313
314/// The parent's side of an allocation: the same `budget_reserved` a
315/// [`Session::reserve`] writes, naming the child the units went to.
316///
317/// An allocation *is* a reservation from the parent's side — units left the
318/// balance and are not coming back — so it is the same kind and folds the
319/// same way.  What [`FIELD_CHILD`] adds is where they went, which is the one
320/// thing a reservation for a call of its own has no answer to.
321fn allocated_event(
322    amount: i64,
323    tag: Option<&str>,
324    scope_id: &str,
325    child: &str,
326) -> Map<String, Value> {
327    let mut data = budget_move_data(amount, tag, scope_id);
328    data.insert(FIELD_CHILD.to_string(), Value::from(child.to_string()));
329    kernel_event(KIND_BUDGET_RESERVED, data)
330}
331
332/// The parent's side of an allocation that did not happen: what was asked
333/// for, the balance it was measured against, and the child that was not
334/// opened.
335fn allocation_refused_event(
336    amount: i64,
337    remaining: i64,
338    tag: Option<&str>,
339    scope_id: &str,
340    child: &str,
341) -> Map<String, Value> {
342    let mut event = refused_event(amount, remaining, tag, scope_id);
343    if let Some(Value::Object(data)) = event.get_mut(super::event::FIELD_DATA) {
344        data.insert(FIELD_CHILD.to_string(), Value::from(child.to_string()));
345    }
346    event
347}
348
349/// Put the caller's labels on a kernel-written event.
350///
351/// `meta` is the envelope key for what a reader groups or filters by, and a
352/// session's opening is the one event a supervisor has to find without
353/// knowing the kind's shape — the run a session belongs to goes here, not in
354/// `data`, so a view reads it the same way it reads a label on any other
355/// event. Absent writes nothing: an unlabelled opening gets no `meta` key
356/// here (the store reads the column back as an empty object), so a select on
357/// a label finds nothing either way.
358fn with_meta(
359    mut event: Map<String, Value>,
360    meta: Option<Map<String, Value>>,
361) -> Map<String, Value> {
362    if let Some(meta) = meta {
363        if !meta.is_empty() {
364            event.insert(super::event::FIELD_META.to_string(), Value::Object(meta));
365        }
366    }
367    event
368}
369
370/// A child's `session_opened`: the scope it opens under, and the stream it
371/// was opened from.
372///
373/// The same event [`Session::open_on`] writes plus [`FIELD_PARENT`], and
374/// written by the parent's store rather than the child's — the opening and
375/// the reservation that paid for it are one transaction, so the child's first
376/// event arrives before the child has a handle at all.
377fn child_opened_event(
378    owner: &str,
379    scope_id: &str,
380    parent: &str,
381    meta: Option<Map<String, Value>>,
382) -> Map<String, Value> {
383    let mut data = Map::new();
384    data.insert(FIELD_OWNER.to_string(), Value::from(owner.to_string()));
385    data.insert(
386        FIELD_SCOPE_ID.to_string(),
387        Value::from(scope_id.to_string()),
388    );
389    data.insert(FIELD_PARENT.to_string(), Value::from(parent.to_string()));
390    with_meta(kernel_event(KIND_SESSION_OPENED, data), meta)
391}
392
393/// A child's `budget_granted`: the units the parent moved, naming where they
394/// came from.
395///
396/// A grant on the child's ledger like any other — its balance is the fold of
397/// its own stream, and this is the entry that starts it — with
398/// [`FIELD_PARENT`] recording that an owner did not conjure it: it was paid
399/// for by a `budget_reserved` on the stream named here, in the same write.
400fn child_granted_event(
401    amount: i64,
402    tag: Option<&str>,
403    scope_id: &str,
404    parent: &str,
405) -> Map<String, Value> {
406    let mut data = budget_move_data(amount, tag, scope_id);
407    data.insert(FIELD_PARENT.to_string(), Value::from(parent.to_string()));
408    kernel_event(KIND_BUDGET_GRANTED, data)
409}
410
411/// The `session_closed` event a close records.
412///
413/// One builder for both close paths — the awaited [`Session::close_with`] and
414/// the detached [`Session::close_detached`] — so a boundary written by the
415/// backstop is the same event, with the same fields, as one a caller asked
416/// for.  The reason defaults to [`DEFAULT_CLOSE_REASON`]; an absent `detail`
417/// is an absent field rather than a null.
418///
419/// `children` are the sessions this one opened that had not ended when it
420/// did, found by the store inside the same transaction that writes this
421/// event.  An empty list is an absent field, not an empty array: "there were
422/// none" and "nobody looked" then read the same way, which is the truth for
423/// the detached path — it cannot scan, so it writes none.
424fn closing_event(
425    reason: Option<&str>,
426    detail: Option<&str>,
427    children: Vec<String>,
428) -> Map<String, Value> {
429    let mut data = Map::new();
430    data.insert(
431        FIELD_REASON.to_string(),
432        Value::from(reason.unwrap_or(DEFAULT_CLOSE_REASON)),
433    );
434    if let Some(detail) = detail {
435        data.insert(FIELD_DETAIL.to_string(), Value::from(detail.to_string()));
436    }
437    if !children.is_empty() {
438        data.insert(
439            FIELD_OPEN_CHILDREN.to_string(),
440            Value::from(children.into_iter().map(Value::from).collect::<Vec<_>>()),
441        );
442    }
443    kernel_event(KIND_SESSION_CLOSED, data)
444}
445
446/// How the store recognises the children of this session: the two kernel
447/// kinds that bracket a session, and the `data` field a child's opening names
448/// its parent in.
449///
450/// Built here rather than in the store because the vocabulary is the
451/// kernel's — [`EventStore::append_with_open_children`] walks a database and
452/// knows nothing about what `session_opened` means.
453fn child_scan() -> ChildScan {
454    ChildScan {
455        opened: KIND_SESSION_OPENED.to_string(),
456        closed: KIND_SESSION_CLOSED.to_string(),
457        parent_field: FIELD_PARENT.to_string(),
458    }
459}
460
461/// What an allocation's decision is shown: the ledger it measures, and the
462/// ending it must not open a child under.
463///
464/// [`BUDGET_KINDS`] plus `session_closed`, written out rather than
465/// concatenated because a `const` cannot join two slices — and held against
466/// the ledger's own list by a test below, so a kind added to the ledger and
467/// missed here goes red instead of quietly falling out of the balance an
468/// allocation is decided against.
469const ALLOCATION_KINDS: &[&str] = &[
470    KIND_BUDGET_GRANTED,
471    KIND_BUDGET_RESERVED,
472    KIND_BUDGET_REFUSED,
473    KIND_BUDGET_SPENT,
474    KIND_SESSION_CLOSED,
475];
476
477/// What a caller should have called instead of hand-appending `kind`.
478///
479/// The refusal names the method that legitimately writes the kind, so the
480/// error is a redirection rather than a wall.
481fn kernel_only_hint(kind: &str) -> &'static str {
482    match kind {
483        KIND_SESSION_OPENED => "a session records its own opening",
484        KIND_SESSION_CLOSED => "use close",
485        _ => "use reserve / spend",
486    }
487}
488
489/// One session: a stream, and the scope it is written under.
490pub struct Session {
491    /// Correlation id, unique per session — the stream this session writes.
492    /// Distinct from [`Scope::id`], which names the authority the stream is
493    /// written under.
494    id: String,
495    /// The session's scope: its kernel-issued id, its owner, and the budget
496    /// an owner granted it.  Held by value — a scope and its session begin
497    /// and end together, so there is nothing to point at.
498    scope: Scope,
499    /// K1 append-only history, held behind the upcasting seam.
500    ///
501    /// A [`CurrentStore`] and never a bare `Box<dyn EventStore>`: the backend
502    /// inside it can be the in-memory store or the durable SQLite one, and
503    /// the seam is what makes every read of it — here and in the folds — an
504    /// event at the current shape.
505    store: CurrentStore,
506    /// The last balance fold, and the store head it was taken at.
507    ///
508    /// Not a counter: nothing adds to it or subtracts from it.  A read of
509    /// the balance compares the store's head against the `seq` recorded here
510    /// and, if the log has moved on, refolds [`fold_balance`] over the
511    /// stream and replaces both halves.  So the answer is the ledger's on a
512    /// stream two handles write to, and costs one head read on a stream that
513    /// has not moved.
514    ///
515    /// Behind a lock because reading a balance is a read —
516    /// [`Session::remaining`] takes `&self`, and the cache it refreshes is
517    /// derived state rather than a change to the session — and because that
518    /// read is an `async fn` now, whose future has to be `Send`; a `Cell`
519    /// would not be.  The guard is never held across an `.await`: the cached
520    /// pair is copied out, the store is asked, and the answer written back.
521    balance: Mutex<(u64, Option<i64>)>,
522    /// Set by `close()`; blocks this handle's further `append` / `reserve` /
523    /// `spend` / `close`.
524    ///
525    /// The handle's state, not the stream's: another handle on the same
526    /// stream keeps its own flag and goes on writing, and the log records
527    /// what it writes.  Nothing consults the log to set this.
528    closed: bool,
529}
530
531impl std::fmt::Debug for Session {
532    /// The store is a trait object (`dyn EventStore` is not `Debug`), so it
533    /// is summarised rather than printed; the fields that identify the
534    /// session are shown.
535    ///
536    /// The length is not among them: reading it is a call to the store now,
537    /// and `Debug` cannot wait for one.  A caller that wants it asks
538    /// [`Session::len`].
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        f.debug_struct("Session")
541            .field("id", &self.id)
542            .field("scope", &self.scope)
543            .field("closed", &self.closed)
544            .finish_non_exhaustive()
545    }
546}
547
548impl Session {
549    /// Open a session for `owner` with an optional budget grant, on an
550    /// in-memory store.
551    ///
552    /// `owner` is total: pass a real principal id, or [`ANON`] / [`SYSTEM`]
553    /// for the reserved ones.  The `session_opened` event is appended here,
554    /// so a fresh session already has one event.
555    ///
556    /// "In-memory" is an in-memory *database*, not a different kind of store:
557    /// the same backend a durable session uses, on the one in-memory log the
558    /// host holds ([`SqliteEventStore::open_memory`]), which is reclaimed when
559    /// the run ends.  There is one backend, because the log is read with SQL
560    /// and a log that cannot be queried would be a second, lesser kind of
561    /// session — and because a stream in it is a stream like any other, so a
562    /// child can be opened from one and a resume can find it by name.
563    ///
564    /// The stream is minted here and adopted as the session's id, so
565    /// [`Session::id`] names the stream this session writes — the same
566    /// identity a durable session has, and what `$stream` binds to in a
567    /// [`Session::query`].
568    pub async fn new(
569        owner: String,
570        grant: Option<BudgetGrant>,
571        meta: Option<Map<String, Value>>,
572        logs: &Logs,
573    ) -> KnlResult<Self> {
574        let stream = uuid::Uuid::new_v4().to_string();
575        let store = SqliteEventStore::open_memory(stream.clone(), logs).await?;
576        let mut session = Self::open_on(owner, grant, meta, Box::new(store)).await?;
577        session.adopt_id(stream);
578        Ok(session)
579    }
580
581    /// Open a session for `owner` on a caller-chosen backend `store` (the
582    /// in-memory store, or the durable SQLite one).
583    ///
584    /// Like [`Session::new`] but takes the backend, so the shell decides
585    /// whether the log is ephemeral or persisted.  It appends the same
586    /// `session_opened` boundary, recording the session's scope on it — the
587    /// kernel-issued [`ScopeId`] and the `owner` — so a later
588    /// [`Session::resume`] can recover the scope from the log alone, and
589    /// the `grant`, so the log says what the owner allowed.
590    /// `session_opened` is an open-shape reserved kind, so both extra fields
591    /// are accepted without any change to the validator.
592    ///
593    /// The two are written as one batch ([`EventStore::append_many`]), so a
594    /// durable stream either carries the opening *and* the quota it opened
595    /// under, or carries nothing at all: an open that fails leaves no session
596    /// behind to close.
597    pub async fn open_on(
598        owner: String,
599        grant: Option<BudgetGrant>,
600        meta: Option<Map<String, Value>>,
601        store: Box<dyn EventStore>,
602    ) -> KnlResult<Self> {
603        // Wrap the chosen backend in the upcasting seam, so every one of this
604        // session's reads (view folds, `events`, the balance fold, the decision
605        // a `reserve` takes inside the store) is checked to be the current
606        // shape by construction.  The chain here is *empty* on purpose: the
607        // durable backend is opened with `kernel_upcasters()` and applies it
608        // to everything it reads, so a second copy up here would run every
609        // step twice (see `event_store::kernel_upcasters`).
610        let store = CurrentStore::new(store, Vec::new());
611        let mut session = Self {
612            id: uuid::Uuid::new_v4().to_string(),
613            // The scope is issued here, before the first event: the
614            // `session_opened` below is already written under it.
615            scope: Scope::new(owner, grant),
616            store,
617            // Nothing folded yet, over a stream with nothing in it: the first
618            // read of the balance sees the head move and folds the ledger the
619            // two appends below are about to write.
620            balance: Mutex::new((0, None)),
621            closed: false,
622        };
623        // The scope rides on the opening: the id the kernel just issued, next
624        // to the owner.  Together they are the whole of what a resume needs to
625        // restore the scope, so the boundary is in the log and not only in
626        // this value — and they are the kind's own fields, so they go under
627        // `data` where the validator requires them.
628        let mut opened = Map::new();
629        opened.insert(
630            FIELD_OWNER.to_string(),
631            Value::from(session.scope.owner().to_string()),
632        );
633        opened.insert(
634            FIELD_SCOPE_ID.to_string(),
635            Value::from(session.scope.id().to_string()),
636        );
637        let started = with_meta(kernel_event(KIND_SESSION_OPENED, opened), meta);
638
639        // The grant is its own fact, right after the boundary: what the
640        // owner allowed is the first entry of the ledger the balance folds
641        // from, not a decoration on the run's opening.
642        let mut opening = vec![started];
643        if let Some(grant) = session.scope.grant().cloned() {
644            opening.push(granted_event(&grant, session.scope.id()));
645        }
646
647        // One write for both.  It CAN fail on a durable backend (a busy
648        // database exhausts its retries) — a session that could not record
649        // its own opening must not exist, so the error surfaces — and because
650        // the two events are one write, a failure leaves the stream *empty*
651        // rather than opened-without-a-grant.  There is no half-opened stream
652        // to close on the way out, which is what the earlier best-effort
653        // `session_closed` here was patching over.
654        //
655        // The session is being built, so it cannot have been closed: the
656        // guarded path `append_kernel` takes has nothing to check yet.
657        session.store.append_many(opening).await?;
658        Ok(session)
659    }
660
661    /// Continue an existing session by re-folding its persisted log.
662    ///
663    /// The `store` already holds a session's events (a reopened SQLite
664    /// stream), so resume does *not* append a new `session_opened` — the
665    /// session already opened.  It reads the whole log once and restores the
666    /// state from it:
667    ///
668    /// - the scope, from the first `session_opened` event: its [`ScopeId`]
669    ///   ([`FIELD_SCOPE_ID`]) and its `owner` ([`FIELD_OWNER`]).  An older
670    ///   log written before either was recorded falls back — to a fresh
671    ///   kernel-issued scope id, and to [`ANON`] — rather than failing the
672    ///   resume;
673    /// - the grant, from the last `budget_granted` the log carries, so a
674    ///   reopened stream goes on keeping a ledger and a refusal still has a
675    ///   `tag` to report.  The balance itself needs no restoring: it is
676    ///   [`fold_balance`] over the stream, and the stream is right there.
677    ///
678    /// A `grant` passed here is the owner granting *again*: it is appended
679    /// as a new `budget_granted` and raises the restored balance, rather
680    /// than replacing it.  Omit it to continue on what is left.  Nothing is
681    /// deducted for the earlier `llm_response` usage — an append never
682    /// charged, and what was consumed is a query view's answer over the
683    /// recorded payloads, not the quota's.
684    ///
685    /// A closed stream is not resumed.  A session is disposable: it opens
686    /// once and ends once, so a log that already carries its `session_closed`
687    /// is an ending, not a state to continue from — the caller opens a new
688    /// session instead.
689    ///
690    /// Nothing is restored for the read side: a resumed session's reads —
691    /// `events`, `tail`, a query — go to the reopened store, so they see the
692    /// whole stream on the first call.
693    pub async fn resume(grant: Option<BudgetGrant>, store: Box<dyn EventStore>) -> KnlResult<Self> {
694        // The upcasting seam goes on first, so the restore below reads the
695        // same projected shape every other read of this session gets: a log
696        // written under an older shape resumes as what it means today, and the
697        // stored bytes stay as they were written.  The chain is the backend's
698        // ([`Session::open_on`]), so what the seam adds here is the check.
699        Self::resume_on(grant, CurrentStore::new(store, Vec::new())).await
700    }
701
702    /// [`Session::resume`] on a store that is already behind the seam.
703    ///
704    /// The body of the resume, split off so a test can hand it a chain of its
705    /// own; the public entry wraps the backend in [`kernel_upcasters`] and
706    /// calls this.
707    async fn resume_on(grant: Option<BudgetGrant>, store: CurrentStore) -> KnlResult<Self> {
708        // Fallible read: a transient busy read or an undecodable row surfaces
709        // here rather than being silently folded into a wrong resumed state.
710        //
711        // Unfiltered, unlike the reads a running session takes: a resume is
712        // restoring the whole of the state, and it is looking for the opening
713        // *whatever it was written as* — the chain may have renamed the kind
714        // on the way through, and a filter selects on the stored name
715        // ([`EventStore::read_kinds`]).
716        let log = store.read(0, usize::MAX).await?;
717
718        // Resuming an empty or mistyped stream is a caller error, not an
719        // anonymous zero session: a real session always opens with a
720        // `session_opened`.
721        let opened = log
722            .iter()
723            .find(|event| event.kind() == KIND_SESSION_OPENED)
724            .ok_or_else(|| {
725                // The caller pointed a resume at a stream that is not a
726                // session — a bad argument, not a damaged log.
727                KnlError::Validation(
728                    "stream has no session to resume (no session_opened event)".to_string(),
729                )
730            })?;
731
732        // …and an ended one is not resumed at all.  There is no reopening
733        // kind, so any `session_closed` in the stream is the session's
734        // ending: a handle that carried on past it would be appending to a
735        // log whose readers were told nothing more was coming.
736        if has_ended(&store).await? {
737            return Err(KnlError::Closed(format!(
738                "{CLOSED} (disposable; open a new session)"
739            )));
740        }
741
742        // The scope rides on the `data` of `session_opened`, where the
743        // validator requires both halves of it.  The fallbacks — the owner to
744        // ANON, the scope id to a fresh kernel-issued one (`Scope::restore`
745        // mints it) — are for a stream an upcaster could not bring all the
746        // way: a session that arrives here missing a field is still a
747        // session, and refusing to resume it would lose the log rather than
748        // protect it.
749        let owner = data_field(opened, FIELD_OWNER)
750            .and_then(Value::as_str)
751            .unwrap_or(ANON)
752            .to_string();
753        let scope_id: Option<ScopeId> = data_field(opened, FIELD_SCOPE_ID)
754            .and_then(Value::as_str)
755            .map(str::to_string);
756
757        // The log was read once already, so seed the balance cache from it
758        // rather than folding the same events again on the first read: the
759        // head it was taken at is the last event's seq (`read` returns events
760        // in seq order).  A resume errors above on an empty /
761        // session_opened-less log, so this is a real event's seq; the `0`
762        // fallback is unreachable but keeps it total.
763        let head = log.last().map(Current::seq).unwrap_or(0);
764
765        // The grant comes back off the log: a resumed session keeps having a
766        // budget (and a tag to report) even when the caller grants nothing
767        // new.  What is left of it is the fold, seeded just below — over the
768        // whole log, which folds to the same balance as the ledger alone
769        // because nothing else moves it.
770        let mut session = Self {
771            id: uuid::Uuid::new_v4().to_string(),
772            scope: Scope::restore(scope_id, owner, last_grant(&log)),
773            store,
774            balance: Mutex::new((head, fold_balance(&log))),
775            closed: false,
776        };
777
778        // A fresh grant is the owner allowing more, so it is recorded like
779        // any other and *adds* to what was left — and only on a stream that
780        // already keeps a ledger ([`Session::grant_on_resume`]).
781        //
782        // A caller that must vet the restored session *before* anything is
783        // written for it — the Lua bridge, which refuses a reserved owner —
784        // resumes with no grant and calls [`Session::grant_on_resume`] once
785        // the stream has passed.
786        if let Some(grant) = grant {
787            session.grant_on_resume(grant).await?;
788        }
789        Ok(session)
790    }
791
792    /// The owner granting again *on a resume*: record `budget_granted`, but
793    /// only on a stream whose ledger already carries one.
794    ///
795    /// **One session, one budget.**  Whether a session has a quota is settled
796    /// when it opens: a stream that opened with a grant keeps a ledger for the
797    /// whole of its life, and a stream that opened without one has no ledger
798    /// and refuses nothing.  A resume may raise the first — that is the owner
799    /// allowing more — and may not create the second, because a session that
800    /// opened unbudgeted has handles that were told there is no quota, and a
801    /// ledger appearing underneath them turns "refuses nothing" into "refuses"
802    /// with nobody having asked for it.  So a `budget` on a resume of a stream
803    /// with no `budget_granted` is a [`KnlError::Validation`]: the caller
804    /// wanted [`Session::open_on`], and nothing is written.
805    ///
806    /// The question is decided **inside the transaction that would write the
807    /// grant** ([`EventStore::append_if`]), not read beforehand: two resumes
808    /// racing on one stream would otherwise both see an empty ledger and both
809    /// create one.
810    ///
811    /// [`Session::grant_more`] is the other door and keeps no such rule: it is
812    /// the owner acting through a handle it holds, on a session it opened,
813    /// rather than a second handle changing what a first one was told.
814    async fn grant_on_resume(&mut self, grant: BudgetGrant) -> KnlResult<()> {
815        // Before anything is decided: an amount the ledger cannot take is the
816        // caller's own bug, and the log should not carry the evidence twice.
817        budget::check_amount(grant.amount)?;
818        let scope_id = self.scope.id().to_string();
819        let recorded = grant.clone();
820
821        let committed = self
822            .store
823            .append_if(
824                Some(BUDGET_KINDS),
825                Box::new(move |events: Vec<Current>| {
826                    last_grant(&events)?;
827                    Some(granted_event(&recorded, &scope_id))
828                }),
829            )
830            .await?;
831
832        if committed.is_none() {
833            return Err(KnlError::Validation(
834                "this stream opened with no budget, and a resume does not give one: a session's \
835                 quota is settled when it opens (open a new session with the grant, or resume \
836                 without one)"
837                    .to_string(),
838            ));
839        }
840        self.scope.grant_more(grant)
841    }
842
843    /// The owner granting again: record `budget_granted` and raise the
844    /// balance by it.
845    ///
846    /// The one way the balance rises, and it is a fact in the log before it
847    /// is a number in the counter — a failed append leaves the balance
848    /// exactly as the ledger describes it.  Refused on a closed session, like
849    /// every other write: a run that has ended cannot be granted more.
850    ///
851    /// This is the owner acting through a handle it holds, so it takes the
852    /// ledger as it finds it — including a stream that has none, which this
853    /// grant then starts.  [`Session::grant_on_resume`] is the other door and
854    /// refuses that case: a *resume* is a second handle arriving at a session
855    /// that already exists, and it does not get to give one a quota it opened
856    /// without.
857    pub async fn grant_more(&mut self, grant: BudgetGrant) -> KnlResult<()> {
858        let event = granted_event(&grant, self.scope.id());
859        self.append_kernel(event).await?;
860        self.scope.grant_more(grant)
861    }
862
863    /// Open a session *from* this one, paying for it out of this session's
864    /// balance — one transaction, both ledgers.
865    ///
866    /// The kernel's whole part in a session tree.  It records two facts and
867    /// performs one move:
868    ///
869    /// - the child's `session_opened` carries [`FIELD_PARENT`] — this
870    ///   session's stream — and the `budget_granted` it opens with carries it
871    ///   too, so where the units came from is in the log beside them;
872    /// - this session's ledger gains a `budget_reserved` naming the child
873    ///   ([`FIELD_CHILD`]).  An allocation is a *spend* from here: the
874    ///   balance falls by exactly what the child's rises by, and nothing is
875    ///   returned when the child closes.  A refund would be a balance rising
876    ///   without an owner granting, which is the one thing the ledger does
877    ///   not allow.
878    ///
879    /// All of it is decided and written inside one transaction on this
880    /// session's store ([`EventStore::append_if_many`]), so two children
881    /// asking at once cannot both be given what only one balance covers, and
882    /// no reader ever meets a child that opened without the reservation that
883    /// paid for it.
884    ///
885    /// **The child is on the parent's database.**  `child_store` must be a
886    /// store on the same database ([`EventStore::database`]) opened on
887    /// `child_stream`; anything else is a [`KnlError::Validation`] before a
888    /// word is written.  A tree spread over two logs could be neither written
889    /// atomically nor read back by one statement, so it is not a tree.
890    ///
891    /// **The child's stream must be empty.**  The two events written over
892    /// there are a session's first, so `child_stream` names a stream nothing
893    /// has been written to; one that already carries an event is a
894    /// [`KnlError::Validation`] with nothing written on either side.  The
895    /// emptiness is decided inside the same transaction as the rest — the
896    /// decision is shown the other stream's first event along with this
897    /// ledger ([`EventStore::append_if_many`]) — because two allocations
898    /// naming one stream would otherwise both be told it was free and both
899    /// open a session on it.
900    ///
901    /// **A refusal is an error here, not a `false`.**  When the balance does
902    /// not cover the allocation, a `budget_refused` naming the child is
903    /// recorded on this session, nothing is opened, and
904    /// [`KnlError::Refused`] is raised: unlike [`Session::reserve`], which is
905    /// asked in a loop that expects to be told no, an allocation either
906    /// produced a session or it did not, and there is no half-opened one to
907    /// hand back.
908    ///
909    /// **The parent must be open.**  This handle having closed is refused
910    /// straight away, and a stream whose log already carries an ending is
911    /// refused *inside the transaction* — the decision is shown
912    /// `session_closed` along with the ledger — both as
913    /// [`KnlError::Closed`], the same answer a resume of a closed stream
914    /// gives.
915    ///
916    /// The child comes back as an ordinary session with its scope restored
917    /// from the events just written ([`Session::resume`] over the two of
918    /// them): its balance is the fold of its own ledger, its owner and scope
919    /// are what the opening recorded, and nothing about it is special
920    /// afterwards.  What it is *not* is a handle this session holds — the
921    /// parent keeps no pointer, and a supervisor reads the structure back out
922    /// of the log.
923    pub async fn open_child(
924        &mut self,
925        child_stream: String,
926        owner: String,
927        allocation: Allocation,
928        meta: Option<Map<String, Value>>,
929        child_store: Box<dyn EventStore>,
930    ) -> KnlResult<Self> {
931        if self.closed {
932            return Err(KnlError::Closed(format!(
933                "{CLOSED} (a child is opened from an open parent)"
934            )));
935        }
936        budget::check_amount(allocation.amount)?;
937
938        // One log, checked before anything is written: the two halves of an
939        // allocation land in one transaction, and a transaction covers one
940        // database.
941        match (self.store.database(), child_store.database()) {
942            (Some(parent), Some(child)) if parent == child => {}
943            (Some(parent), Some(child)) => {
944                return Err(KnlError::Validation(format!(
945                    "a child opens on its parent's database, and a tree is one log: the parent is \
946                     on {parent:?} and the child was given {child:?}"
947                )));
948            }
949            _ => {
950                return Err(KnlError::Validation(
951                    "a child opens on its parent's database, and one of the two stores keeps a \
952                     single stream with no database to share"
953                        .to_string(),
954                ));
955            }
956        }
957
958        let amount = allocation.amount;
959        // The units come out of this ledger, so they are counted in its unit
960        // unless the caller renamed them for the child.
961        let parent_tag = self.scope.grant().and_then(|grant| grant.tag.clone());
962        let child_tag = allocation.tag.clone().or_else(|| parent_tag.clone());
963        let parent_scope = self.scope.id().to_string();
964        let parent_id = self.id.clone();
965        let child_id = child_stream.clone();
966
967        // The child's scope is issued before its first event, exactly as
968        // `open_on` issues one.  The value is not kept: the handle below is
969        // built by a resume, which restores the scope from the very
970        // `session_opened` this id is about to be written onto.
971        let child_scope_id = Scope::new(owner.clone(), None).id().to_string();
972
973        // What the decision saw, carried out of it: the decision runs on the
974        // store's own thread, so the balance a refusal reports, the ending it
975        // found and the stream it found already in use come back through cells
976        // rather than through the events it returns.
977        let ended = Arc::new(AtomicBool::new(false));
978        let occupied = Arc::new(AtomicBool::new(false));
979        let refused = Arc::new(AtomicBool::new(false));
980        let measured = Arc::new(AtomicI64::new(0));
981        let (found_ending, found_events, said_no, balance_seen) = (
982            Arc::clone(&ended),
983            Arc::clone(&occupied),
984            Arc::clone(&refused),
985            Arc::clone(&measured),
986        );
987
988        let committed = self
989            .store
990            .append_if_many(
991                &child_stream,
992                Some(ALLOCATION_KINDS),
993                Box::new(move |seen: Split<Current>| {
994                    // A child opens once, on a stream of its own.  The two
995                    // events written over there are a session's *first*, so a
996                    // stream that already carries any is not a child being
997                    // opened but an existing log being written into — a second
998                    // `session_opened` under a scope its earlier handles never
999                    // heard of, and a `budget_granted` nobody's owner allowed.
1000                    //
1001                    // Inside the invariant for the same reason the ending is:
1002                    // asked before the transaction, two allocations naming one
1003                    // stream would both be told it was empty.  Nothing is
1004                    // written on either side — this is the caller having given
1005                    // a bad argument, like a child store on another database,
1006                    // and there is no fact about the parent's ledger in it.
1007                    if !seen.other.is_empty() {
1008                        found_events.store(true, Ordering::Relaxed);
1009                        return None;
1010                    }
1011                    let events = seen.own;
1012                    // The ending is part of the invariant, not a check taken
1013                    // beforehand: a parent that closed between the read and
1014                    // the write would otherwise get a child anyway.
1015                    if events.iter().any(|e| e.kind() == KIND_SESSION_CLOSED) {
1016                        found_ending.store(true, Ordering::Relaxed);
1017                        return None;
1018                    }
1019                    // No grant on the parent is no ledger to measure against
1020                    // — the same rule `reserve` follows — so the allocation
1021                    // is allowed and recorded, and the fold ignores a
1022                    // reservation with nothing granted before it.
1023                    if let Some(balance) = fold_balance(&events) {
1024                        if balance < amount {
1025                            said_no.store(true, Ordering::Relaxed);
1026                            balance_seen.store(balance, Ordering::Relaxed);
1027                            return Some(Split::own(vec![allocation_refused_event(
1028                                amount,
1029                                balance,
1030                                parent_tag.as_deref(),
1031                                &parent_scope,
1032                                &child_id,
1033                            )]));
1034                        }
1035                    }
1036                    Some(Split {
1037                        own: vec![allocated_event(
1038                            amount,
1039                            parent_tag.as_deref(),
1040                            &parent_scope,
1041                            &child_id,
1042                        )],
1043                        other: vec![
1044                            child_opened_event(&owner, &child_scope_id, &parent_id, meta.clone()),
1045                            child_granted_event(
1046                                amount,
1047                                child_tag.as_deref(),
1048                                &child_scope_id,
1049                                &parent_id,
1050                            ),
1051                        ],
1052                    })
1053                }),
1054            )
1055            .await?;
1056
1057        // Read before the ending is, because a decision that found the child's
1058        // stream occupied returned before it looked at the parent at all: the
1059        // stream was the wrong argument, and reporting it as a closed parent
1060        // would send the caller after the wrong thing.
1061        if occupied.load(Ordering::Relaxed) {
1062            return Err(KnlError::Validation(format!(
1063                "a child opens on a stream of its own, and {child_stream:?} already has events on \
1064                 it: pass a stream nothing has been written to (nothing was written here, on \
1065                 either side)"
1066            )));
1067        }
1068        if committed.is_none() || ended.load(Ordering::Relaxed) {
1069            return Err(KnlError::Closed(format!(
1070                "{CLOSED} (the parent's log already carries its ending)"
1071            )));
1072        }
1073        if refused.load(Ordering::Relaxed) {
1074            let balance = measured.load(Ordering::Relaxed);
1075            return Err(KnlError::Refused(format!(
1076                "the parent's balance is {balance}, which does not cover an allocation of \
1077                 {amount}; the refusal is in the log and no child was opened"
1078            )));
1079        }
1080
1081        // The opening and the grant are committed, so the child's stream is a
1082        // session: resuming it restores the scope and folds the balance out
1083        // of the two events that were just written for it.
1084        let mut child = Self::resume(None, child_store).await?;
1085        child.adopt_id(child_stream);
1086        Ok(child)
1087    }
1088
1089    /// The session-correlation id.
1090    pub fn id(&self) -> &str {
1091        &self.id
1092    }
1093
1094    /// Adopt `id` as the session-correlation id.
1095    ///
1096    /// Used so a session and the SQLite stream it writes to share one id:
1097    /// `open_on` / `resume` mint a fresh id, and the caller that opened the
1098    /// stream overrides it to that stream, so the id a caller resumes by *is*
1099    /// the stream — and so `$stream` in a [`Session::query`] means this
1100    /// session's own rows.  `session_opened` records the scope (its id and
1101    /// the `owner`), never the session id, so overriding the id does not
1102    /// desync the log.
1103    pub(crate) fn adopt_id(&mut self, id: impl Into<String>) {
1104        self.id = id.into();
1105    }
1106
1107    /// The scope this session is written under.
1108    ///
1109    /// The scope and the session are two things with one lifetime: this is
1110    /// the authority half — the id, the owner, the granted quota — while the
1111    /// session is the stream.  [`Session::owner`] / [`Session::scope_id`]
1112    /// read through to it for the two call sites that want one field.
1113    pub fn scope(&self) -> &Scope {
1114        &self.scope
1115    }
1116
1117    /// The scope's kernel-issued id, as recorded on `session_opened` and on
1118    /// every `budget_*` event.
1119    ///
1120    /// Not the session id: [`Session::id`] names the stream, this names the
1121    /// authority the stream is written under.
1122    pub fn scope_id(&self) -> &str {
1123        self.scope.id()
1124    }
1125
1126    /// Whose scope this is (a principal id, or [`ANON`] / [`SYSTEM`]).
1127    pub fn owner(&self) -> &str {
1128        self.scope.owner()
1129    }
1130
1131    /// The database this session's log lives in, or `None` for a backend that
1132    /// is not one ([`EventStore::database`]).
1133    ///
1134    /// Published for one caller: whoever opens a child has to open its store
1135    /// on the parent's database, and asking the parent is how it knows which
1136    /// that is ([`Session::open_child`] refuses any other).  It is an
1137    /// identity to pass along, not a location to take apart.
1138    pub fn database(&self) -> Option<&str> {
1139        self.store.database()
1140    }
1141
1142    /// Record an event, returning its `seq`.  The one write path.
1143    ///
1144    /// Any kind is welcome, the reserved ones included, as long as it meets
1145    /// the shape its kind requires and is not one of the kernel's own
1146    /// ([`is_kernel_only`]).  The kernel-owned `seq` / `epoch_ms` are
1147    /// stamped here and overwrite any caller-supplied value; nothing else
1148    /// is added, and the labels the caller declared — `meta.beat` among
1149    /// them — are recorded as given.
1150    ///
1151    /// No append moves the budget, this one included.  A deduction is asked
1152    /// for before a call ([`Session::reserve`]) or taken after it
1153    /// ([`Session::spend`]) by the layer that knows what a call costs; the
1154    /// history records what happened and says nothing about what was
1155    /// allowed.
1156    ///
1157    /// The session's own boundaries are not appendable: `session_opened` and
1158    /// `session_closed` are written by [`Session::open_on`] and
1159    /// [`Session::close`], and a caller asking for either is refused.
1160    ///
1161    /// The append lands.  The store assigns the `seq` and serializes the
1162    /// write per stream, so two handles on one stream both record and the log
1163    /// interleaves in the order the writes arrived.  A stale view of the head
1164    /// is not a reason to refuse a fact: [`Session::head`] is what this handle
1165    /// last saw, and nothing is compared against it.
1166    ///
1167    /// The one refusal is this handle having closed.  Another handle's close
1168    /// is not: it set *that* handle's flag, and a write landing after the
1169    /// `session_closed` it wrote is recorded, because that is what happened.
1170    pub async fn append(&mut self, event: Map<String, Value>) -> KnlResult<u64> {
1171        // The kernel's own kinds are refused before the closed check has
1172        // anything to say about it — the kind is wrong whatever state the
1173        // session is in.  The balance is a fold of the `budget_*` events, so
1174        // accepting one from a caller would be letting it grant itself the
1175        // quota its owner set; the two `session_*` events are the lifecycle
1176        // a resume and an audit read off the log.
1177        let kind = event.get(FIELD_KIND).and_then(Value::as_str).unwrap_or("");
1178        if is_kernel_only(kind) {
1179            return Err(KnlError::Validation(format!(
1180                "{kind:?} is written by the kernel only ({})",
1181                kernel_only_hint(kind)
1182            )));
1183        }
1184        self.append_kernel(event).await
1185    }
1186
1187    /// [`Session::append`] without the kernel-only guard: the path the
1188    /// kernel's own writes take.
1189    ///
1190    /// Same validation, same stamping, same serialized write — the only
1191    /// difference is that this one may write the kernel-only kinds, which is
1192    /// what makes "the kernel wrote it" a property of the code path rather
1193    /// than of a field a caller could set.
1194    ///
1195    /// A plain append, with no invariant to decide: the only refusal is the
1196    /// handle's own `closed` flag, checked here before the store is touched.
1197    /// The store is not asked whether the stream has ended, because a write
1198    /// arriving after an ending is a fact — evidence of a bug or a misuse —
1199    /// and dropping it would hide the one thing an audit is reading for.
1200    async fn append_kernel(&mut self, event: Map<String, Value>) -> KnlResult<u64> {
1201        if self.closed {
1202            return Err(KnlError::Closed(CLOSED.to_string()));
1203        }
1204
1205        // The store orders the write and hands back where it landed.
1206        let committed = self.store.append(event).await?;
1207        Ok(committed.seq)
1208    }
1209
1210    /// Events with `seq >= from`, at most `limit`, cloned, at the current
1211    /// shape.
1212    ///
1213    /// They come back as [`Current`]s: the read went through the upcaster
1214    /// seam, and the type says so, so a caller folding them cannot be folding
1215    /// a shape that has been superseded.  A caller that needs to own the
1216    /// underlying object — the Lua bridge, building tables — takes it with
1217    /// [`Current::into_inner`].
1218    ///
1219    /// **The caller says how much it will take.**  A stream grows without
1220    /// bound, and every event of it read here is decoded, upcasted, cloned and
1221    /// (across the bridge) turned into a Lua table — so an unbounded read is
1222    /// an unbounded allocation on the VM's own thread.  `limit` is the
1223    /// caller's answer to that; the shell reads a page at a time and pages
1224    /// with `from` ([`super::query::DEFAULT_LIMIT`] is what the bridge asks
1225    /// for).  `usize::MAX` is still spelled out where a caller really does
1226    /// want the whole stream — a restore fold, a test — and says so at the
1227    /// call.
1228    ///
1229    /// Fallible: a durable backend can hit a transient busy read or a row it
1230    /// cannot decode, which surfaces here rather than being dropped silently.
1231    /// The in-memory backend is always `Ok`.
1232    pub async fn events(&self, from: u64, limit: usize) -> KnlResult<Vec<Current>> {
1233        self.store.read(from, limit).await
1234    }
1235
1236    /// Number of recorded events.  Fallible like [`Session::events`].
1237    pub async fn len(&self) -> KnlResult<usize> {
1238        self.store.len().await
1239    }
1240
1241    /// Whether the history is empty (only before `session_opened`, i.e.
1242    /// never for a session built by [`Session::new`]).
1243    pub async fn is_empty(&self) -> KnlResult<bool> {
1244        self.store.is_empty().await
1245    }
1246
1247    /// Ask the budget to allow `amount`: `true` when it was deducted, `false`
1248    /// when the balance would not cover it (and nothing was deducted).
1249    ///
1250    /// The stop the budget exists for.  A caller asks before it spends, and
1251    /// a `false` is a planned halt with the balance untouched — not a
1252    /// failure, and not a state the run has to be rolled back out of.
1253    ///
1254    /// **Whether there is a budget at all is the log's answer, not this
1255    /// handle's.**  The decision is shown the stream's `budget_*` events, and
1256    /// a ledger with no `budget_granted` in it is a run with no quota: nothing
1257    /// is decided, nothing is recorded, and the answer is `true`.  A grant the
1258    /// log *does* carry is honoured even by a handle that was opened without
1259    /// one — two handles on one stream cannot disagree about whether it has a
1260    /// budget, because neither of them is asked.  The scope's cached grant is
1261    /// a hint about the words (§ [`Session::grant`]) and never the authority.
1262    ///
1263    /// **Both answers are recorded, by the same decision.**  A
1264    /// `budget_reserved` when the balance covered it, a `budget_refused`
1265    /// (carrying what was asked for and what there was) when it did not —
1266    /// whichever the decision built lands in the transaction that took it, so
1267    /// exactly one of the two is in the log and this call's answer is which
1268    /// one that was.  Recording the refusal afterwards, as a second append,
1269    /// made a refusal that could not be written indistinguishable from a
1270    /// storage failure with nothing decided.
1271    ///
1272    /// This is a *command with an invariant*, so the decision is taken inside
1273    /// the store ([`EventStore::append_if`]): the backend hands the ledger to
1274    /// [`fold_balance`] and writes the decision's event in the same serialized
1275    /// write, so two handles on one stream cannot both reserve the same
1276    /// allowance.  Nothing is set afterwards: the write moved the store's
1277    /// head, so the next read of [`Session::remaining`] refolds the ledger the
1278    /// entry is now part of.
1279    ///
1280    /// A handle that has closed refuses, like [`Session::spend`]; another
1281    /// handle's close is nothing to this one — the balance is the whole of the
1282    /// invariant.
1283    pub async fn reserve(&mut self, amount: i64) -> KnlResult<bool> {
1284        if self.closed {
1285            return Err(KnlError::Closed(CLOSED.to_string()));
1286        }
1287        budget::check_amount(amount)?;
1288        let scope_id = self.scope.id().to_string();
1289
1290        // Which of the two entries the decision built, carried back out of
1291        // it: the decision runs wherever the store serializes its writes —
1292        // the connection's own thread — so what it decided comes back through
1293        // a cell rather than through the `Committed` it returns, exactly as
1294        // [`Session::open_child`] carries its own refusal back.  It is set at
1295        // the moment the refusal event is built, and that event is the one
1296        // the transaction writes, so the flag *is* which kind landed.
1297        let refused = Arc::new(AtomicBool::new(false));
1298        let said_no = Arc::clone(&refused);
1299        let decided_scope = scope_id.clone();
1300        // The whole of the decision: is there a ledger, and does it cover
1301        // what was asked.  Whether the stream carries an ending is not part
1302        // of it — a reservation past the boundary is a fact about a run that
1303        // overran its own close, and the log is where facts go.
1304        //
1305        // The decision names the kinds it folds, so the store hands it the
1306        // ledger and not the whole stream: the invariant is exact either way,
1307        // and this way it costs the size of the ledger.
1308        let committed = self
1309            .store
1310            .append_if(
1311                Some(BUDGET_KINDS),
1312                Box::new(move |events: Vec<Current>| {
1313                    // No `budget_granted` in the log is no ledger to move, so
1314                    // there is nothing to decide and nothing to record.  The
1315                    // grant is also where the unit comes from: the tag is read
1316                    // off the log's own last grant rather than off whatever
1317                    // this handle happens to remember.
1318                    let grant = last_grant(&events)?;
1319                    let balance = fold_balance(&events).unwrap_or(0);
1320                    if balance >= amount {
1321                        return Some(budget_move_event(
1322                            KIND_BUDGET_RESERVED,
1323                            amount,
1324                            grant.tag.as_deref(),
1325                            &decided_scope,
1326                        ));
1327                    }
1328                    said_no.store(true, Ordering::Relaxed);
1329                    Some(refused_event(
1330                        amount,
1331                        balance,
1332                        grant.tag.as_deref(),
1333                        &decided_scope,
1334                    ))
1335                }),
1336            )
1337            .await?;
1338
1339        // A refusal that landed is the only `false`.  Nothing committed means
1340        // the log carries no ledger, which is the run with no quota: it is
1341        // allowed, and there is nothing to write about it.
1342        match (committed, refused.load(Ordering::Relaxed)) {
1343            (Some(_), true) => Ok(false),
1344            _ => Ok(true),
1345        }
1346    }
1347
1348    /// Deduct `amount` from the budget without asking.
1349    ///
1350    /// The other half of [`Session::reserve`], and an independent one: a
1351    /// reserve is a deduction that *refuses* when the balance is short, a
1352    /// spend is a deduction that does not ask — it floors at `0` rather than
1353    /// refusing.  Neither holds anything for the other to release, so calling
1354    /// both for one beat deducts twice; the layer above decides which of them
1355    /// a beat uses.  It is recorded as a `budget_spent`, which is the whole of
1356    /// the move.
1357    ///
1358    /// **Whether there is a budget at all is the log's answer**, exactly as it
1359    /// is for [`Session::reserve`]: the decision is shown the ledger, and a
1360    /// stream with no `budget_granted` in it has no account to move, so
1361    /// nothing is written.  That question is inside the same transaction as
1362    /// the write for the same reason the balance is — a handle's memory of
1363    /// what it opened with is not what the ledger says.
1364    ///
1365    /// There is no *balance* invariant to hold, so a spend is never refused
1366    /// for what the account holds.  What the decision decides is only whether
1367    /// there is an account.
1368    ///
1369    /// **The write is the result.**  It used to hand back the balance it read
1370    /// afterwards, which made a `spend` that landed and then failed its
1371    /// read-back indistinguishable from one that never landed: the caller got
1372    /// an error either way and could not tell whether the deduction was in
1373    /// the log.  Two questions, two calls — this one says the move was
1374    /// recorded, and [`Session::remaining`] says what is left, failing on its
1375    /// own terms.
1376    ///
1377    /// A handle that has closed refuses before the store is reached; another
1378    /// handle's close does not, and a deduction landing after one is recorded
1379    /// as what it is.
1380    pub async fn spend(&mut self, amount: i64) -> KnlResult<()> {
1381        if self.closed {
1382            return Err(KnlError::Closed(CLOSED.to_string()));
1383        }
1384        budget::check_amount(amount)?;
1385        let scope_id = self.scope.id().to_string();
1386
1387        self.store
1388            .append_if(
1389                Some(BUDGET_KINDS),
1390                Box::new(move |events: Vec<Current>| {
1391                    // No `budget_granted` in the log is no ledger to move, and
1392                    // the log's own last grant is where the unit comes from.
1393                    let grant = last_grant(&events)?;
1394                    Some(budget_move_event(
1395                        KIND_BUDGET_SPENT,
1396                        amount,
1397                        grant.tag.as_deref(),
1398                        &scope_id,
1399                    ))
1400                }),
1401            )
1402            .await?;
1403        Ok(())
1404    }
1405
1406    /// The grant this run opened (or resumed) with, if any.
1407    ///
1408    /// Read for its words — the `tag` a caller reports when a reservation
1409    /// is refused — and for its presence, which is what says this session
1410    /// keeps a ledger at all.  The amount on it is the *last* grant, not
1411    /// what is left: that is [`Session::remaining`].
1412    pub fn grant(&self) -> Option<&BudgetGrant> {
1413        self.scope.grant()
1414    }
1415
1416    /// The remaining balance: `Ok(None)` without a budget.
1417    ///
1418    /// The ledger's answer, not a counter's: [`fold_balance`] over the
1419    /// stream, so a handle that has written nothing still sees what another
1420    /// handle spent.  The fold is cached against the store's head and retaken
1421    /// only when the head has moved, so a read on a quiet stream costs one
1422    /// head query.
1423    ///
1424    /// Fallible, and deliberately so.  A store that cannot be read has *no*
1425    /// answer to give, and the two answers this call can otherwise hand back
1426    /// — the last fold, or `None` — both read as facts about the budget:
1427    /// "you have this much" and "there is no budget here".  Serving either
1428    /// off a failed read would fold a failure into a value, and the caller
1429    /// most likely to act on it is a loop deciding whether it may go on
1430    /// spending.  So the failure surfaces, and what to do about a transient
1431    /// busy read ([`KnlError::is_retryable`]) is the caller's to decide.
1432    pub async fn remaining(&self) -> KnlResult<Option<i64>> {
1433        // Copied out and the guard released: nothing below waits while it is
1434        // held.
1435        let (folded_head_seq, cached) =
1436            *self.balance.lock().unwrap_or_else(PoisonError::into_inner);
1437
1438        let head = self.store.head().await?.unwrap_or(0);
1439        // The log has not moved since the fold, so neither has the balance.
1440        if head <= folded_head_seq {
1441            return Ok(cached);
1442        }
1443
1444        // Only the ledger is folded — the balance is a fold of the `budget_*`
1445        // kinds and nothing else — while the *head* it is recorded against is
1446        // the whole stream's, so any write at all makes the next read refold.
1447        // Conservative in the safe direction: an event that moves no balance
1448        // costs one extra fold, never a stale answer.
1449        let ledger = self
1450            .store
1451            .read_kinds(Some(BUDGET_KINDS), 0, usize::MAX)
1452            .await?;
1453        let balance = fold_balance(&ledger);
1454        *self.balance.lock().unwrap_or_else(PoisonError::into_inner) = (head, balance);
1455        Ok(balance)
1456    }
1457
1458    /// Whether the budget is used up (never true without a budget).
1459    ///
1460    /// The same fold [`Session::remaining`] reads, asked as a question — and
1461    /// fallible for the same reason: a `false` that meant "the store could
1462    /// not be read" is the one answer a run must never be given, because it
1463    /// reads as "carry on".
1464    pub async fn exhausted(&self) -> KnlResult<bool> {
1465        Ok(matches!(self.remaining().await?, Some(remaining) if remaining <= 0))
1466    }
1467
1468    /// Whether the session has ended.
1469    pub fn is_closed(&self) -> bool {
1470        self.closed
1471    }
1472
1473    /// End the session, recording `session_closed` with `reason`
1474    /// (defaulting to [`DEFAULT_CLOSE_REASON`]).
1475    ///
1476    /// Idempotent *per handle*: closing a session this handle already closed
1477    /// records nothing.  Another handle closing the same stream is a second
1478    /// ending in the log — the truthful record of two handles both believing
1479    /// they owned the session, and the shape an audit needs to see.
1480    ///
1481    /// **Open children are recorded, never a refusal.**  In the same write,
1482    /// the store looks for the streams that name this session as their parent
1483    /// and carry no ending of their own ([`Session::open_child`]); if it
1484    /// finds any, their ids go on the boundary as
1485    /// `data.open_children`.  The close still succeeds — the log turns no
1486    /// write away, and a run that ended while what it started was still going
1487    /// is exactly the fact worth having in it.
1488    ///
1489    /// Fallible on a durable backend: the `session_closed` append can fail on
1490    /// a database that stays contended past its retries, or a store that is
1491    /// gone.  On failure the session stays open (closed is not set), so the
1492    /// caller knows the boundary was NOT recorded and can retry — a close
1493    /// that reports success with no `session_closed` in the log would
1494    /// silently break resume/audit reads.
1495    pub async fn close(&mut self, reason: Option<&str>) -> KnlResult<()> {
1496        self.close_with(reason, None).await
1497    }
1498
1499    /// [`Session::close`] with a free-text `detail` recorded beside the
1500    /// reason.
1501    ///
1502    /// The reason names *which kind of ending* this was, and stays a short
1503    /// vocabulary a reader can fold on; `detail` is the sentence that only
1504    /// this close can tell — the message of the error that ended the scope.
1505    /// Keeping them apart is what stops every distinct error message from
1506    /// becoming its own reason.
1507    ///
1508    /// Idempotent and fallible exactly like [`Session::close`].
1509    pub async fn close_with(
1510        &mut self,
1511        reason: Option<&str>,
1512        detail: Option<&str>,
1513    ) -> KnlResult<()> {
1514        if self.closed {
1515            return Ok(());
1516        }
1517        // Owned, because the event is built on the store's own thread: the
1518        // decision below travels there with the scan it is answered from.
1519        let reason = reason.map(str::to_string);
1520        let detail = detail.map(str::to_string);
1521
1522        // The boundary and the scan that finds this session's open children
1523        // are one write.  A close is never refused for them — the log turns
1524        // nothing away, and a run that ended while what it started was still
1525        // going is the fact an audit is reading for — so what the scan
1526        // produces is recorded on the event rather than raised at the caller.
1527        //
1528        // Kernel-only kinds do not go through the guarded `append`, which
1529        // would refuse the very event that ends the session; this path is the
1530        // kernel's own, like `append_kernel`, and carries the same `closed`
1531        // check above.
1532        //
1533        // The flag moves only after the boundary landed, so a failed write
1534        // leaves this handle open and the caller free to retry — a close that
1535        // reported success with nothing in the log would break every later
1536        // read of it.
1537        self.store
1538            .append_with_open_children(
1539                &child_scan(),
1540                Box::new(move |children| {
1541                    closing_event(reason.as_deref(), detail.as_deref(), children)
1542                }),
1543            )
1544            .await?;
1545        self.closed = true;
1546        Ok(())
1547    }
1548
1549    /// End the session by handing `session_closed` to the store and *not*
1550    /// waiting for it — the drop backstop.
1551    ///
1552    /// The one close path with nobody left to tell.  A handle that was
1553    /// collected without ever being closed is being dropped right now, on the
1554    /// VM's own thread, inside a Lua collection cycle: there is no task to
1555    /// suspend in and nothing that may block, so the event goes to the store's
1556    /// own writer ([`super::EventStore::detach_append`]) and whether it landed
1557    /// is reported to the log rather than to a caller.
1558    ///
1559    /// The connection thread outlives this handle — its driver belongs to the
1560    /// host, not to the session ([`super::Logs`]) — so the submitted
1561    /// event is still executed, and the host's shutdown drains it.
1562    ///
1563    /// Idempotent per handle, like [`Session::close`]: a session this handle
1564    /// already closed records nothing.
1565    pub fn close_detached(&mut self, reason: &str) {
1566        if self.closed {
1567            return;
1568        }
1569        self.store
1570            .detach_append(closing_event(Some(reason), None, Vec::new()));
1571        self.closed = true;
1572    }
1573
1574    /// A named projection over the history.
1575    ///
1576    /// `tail` is the only name, and it reads `opts.n` (default
1577    /// [`projection::DEFAULT_TAIL_N`]) events from the end.  An unknown name
1578    /// is an error — the vocabulary is closed on purpose, and it is as short
1579    /// as it goes: a projection whose shape depends on what the caller does
1580    /// with it is built above the kernel, from [`Session::events`] or with
1581    /// SQL over the published schema ([`Session::query`]).  The token
1582    /// account is one of those now: it reads the `llm_response` payload,
1583    /// which is the shell's vocabulary and not the kernel's.
1584    ///
1585    /// `&mut self` because the signature belongs to the vocabulary rather
1586    /// than to today's members of it — a fold the kernel names again may
1587    /// keep a cache, and a caller should not have to be recompiled when one
1588    /// does.
1589    pub async fn view(
1590        &mut self,
1591        name: &str,
1592        opts: Option<&Map<String, Value>>,
1593    ) -> KnlResult<Value> {
1594        match name {
1595            VIEW_TAIL => {
1596                let n = tail_count(opts)?;
1597                // Asked for from the end, not sliced off the front: the store
1598                // reads `n` rows ([`super::event_store::EventStore::read_last`])
1599                // rather than handing over the whole stream for the projection
1600                // to throw most of away.  `tail_of` still renders the value,
1601                // and on a slice that is already at most `n` long it is the
1602                // identity — the cut is in the read now.
1603                let events = self.store.read_last(n).await?;
1604                Ok(projection::tail_of(&events, n))
1605            }
1606            other => Err(KnlError::Validation(format!("unknown view {other:?}"))),
1607        }
1608    }
1609
1610    /// Read the log with SQL.
1611    ///
1612    /// The other half of [`Session::view`], and the reason that list of names
1613    /// can stay short: a fold whose shape is the caller's — beats grouped,
1614    /// tool calls paired against their results, a ledger — is a `SELECT`
1615    /// against the table the log lives in, not a name the kernel had to be
1616    /// taught.  What the kernel keeps is the boundary around it
1617    /// ([`super::query`]): one statement, and it reads; a connection that
1618    /// cannot write; values bound rather than pasted; a deadline; a row cap.
1619    ///
1620    /// Two names are reserved.  `$stream` is this session's own stream, and
1621    /// `$sessions` is the set in `opts.sessions` — the session's own stream
1622    /// when that is omitted — expanded to one bound placeholder per id, so
1623    /// reading across a tree of sessions is one statement rather than a loop
1624    /// of them.  The kernel does not judge the set: which streams a caller
1625    /// may read is a question about who the caller is, and that lives above
1626    /// the kernel.
1627    ///
1628    /// Reads keep working after this handle closed, like every other read
1629    /// here: the record outlives the session.
1630    pub async fn query(
1631        &self,
1632        sql: &str,
1633        params: QueryParams,
1634        opts: &QueryOpts,
1635    ) -> KnlResult<QueryRows> {
1636        let plan = query::plan(sql, params, opts, &self.id)?;
1637        self.store.query(&plan).await
1638    }
1639}
1640
1641#[cfg(test)]
1642mod tests {
1643    use super::*;
1644    use crate::knl::event::{kind_of, FIELD_BEAT, FIELD_DATA, FIELD_META};
1645    // The `Vec`-backed test store: the SPI, the seam and the folds are worth
1646    // exercising without a database underneath, and the failure injection
1647    // below is easier to build on a `Vec` than on a connection.
1648    use crate::knl::event_store::MemEventStore;
1649    use serde_json::json;
1650
1651    /// Object map for an event literal.
1652    fn obj(value: Value) -> Map<String, Value> {
1653        match value {
1654            Value::Object(map) => map,
1655            other => panic!("test fixture must be an object, got {other}"),
1656        }
1657    }
1658
1659    /// A grant of `amount`, tagged the way the shell tags one.
1660    fn grant(amount: i64) -> BudgetGrant {
1661        BudgetGrant {
1662            amount,
1663            tag: Some("tokens".to_string()),
1664            desc: None,
1665        }
1666    }
1667
1668    /// The logs every ephemeral test session opens against.
1669    ///
1670    /// One collection for the whole test binary, because that is the shape a
1671    /// host has: an in-memory log belongs to a [`Logs`], so a store opened
1672    /// *beside* a session — a child's, or a second handle on its stream — has
1673    /// to be opened against the same collection to find the same database.
1674    /// Each session is a stream under a fresh id, so sharing the log shares
1675    /// nothing else.
1676    fn test_logs() -> Logs {
1677        static LOGS: std::sync::OnceLock<Logs> = std::sync::OnceLock::new();
1678        LOGS.get_or_init(Logs::new).clone()
1679    }
1680
1681    /// A session owned by the reserved anonymous principal.
1682    ///
1683    /// With a budget it opens with two events, not one: `session_opened` and
1684    /// the `budget_granted` that records what the owner allowed.
1685    async fn new_session(budget: Option<i64>) -> Session {
1686        Session::new(ANON.to_string(), budget.map(grant), None, &test_logs())
1687            .await
1688            .expect("open")
1689    }
1690
1691    /// The balance the log implies, for checking the counter against it.
1692    async fn folded(s: &Session) -> Option<i64> {
1693        fold_balance(&s.events(0, usize::MAX).await.expect("events"))
1694    }
1695
1696    /// A raw backend read, as the folds take it.
1697    ///
1698    /// The tests that verify a durable stream reopen the backend directly —
1699    /// outside the seam, on purpose, to see what was really written — so
1700    /// they say where their `Current`s come from.
1701    fn as_current(events: Vec<Value>) -> Vec<Current> {
1702        events.into_iter().map(Current::assume_current).collect()
1703    }
1704
1705    /// The kinds of `events`, in order.
1706    fn kinds(events: &[Current]) -> Vec<&str> {
1707        events.iter().map(Current::kind).collect()
1708    }
1709
1710    /// The balance, as a test that is not about failure reads it.
1711    ///
1712    /// [`Session::remaining`] is fallible because a store that cannot be read
1713    /// has no balance to report; the stores these tests drive do not fail, so
1714    /// an error here is a broken fixture rather than an outcome to assert on.
1715    /// The tests that *are* about a failing store call the method directly.
1716    async fn remaining(session: &Session) -> Option<i64> {
1717        session.remaining().await.expect("the balance was readable")
1718    }
1719
1720    /// [`Session::exhausted`], read the same way and for the same reason.
1721    async fn exhausted(session: &Session) -> bool {
1722        session.exhausted().await.expect("the balance was readable")
1723    }
1724
1725    /// The `budget_*` events of a session, in seq order.
1726    async fn ledger(s: &Session) -> Vec<Current> {
1727        s.events(0, usize::MAX)
1728            .await
1729            .expect("events")
1730            .into_iter()
1731            .filter(|e| e.kind().starts_with("budget_"))
1732            .collect()
1733    }
1734
1735    /// An `llm_response` event charging `tokens`.
1736    ///
1737    /// A kind of the shell's, so its shape is the shell's too: the kernel
1738    /// takes the envelope and keeps whatever is under `data` verbatim.
1739    fn response(tokens: i64) -> Map<String, Value> {
1740        obj(json!({
1741            "kind": "llm_response",
1742            "data": {
1743                "content": [{ "type": "text", "text": "ok" }],
1744                "usage": { "input_tokens": tokens },
1745                "stop_reason": "end_turn"
1746            }
1747        }))
1748    }
1749
1750    /// A `data` field of a recorded event, for the assertions below.
1751    fn field<'a>(event: &'a Current, name: &str) -> &'a Value {
1752        data_field(event, name).unwrap_or_else(|| panic!("data.{name} is missing: {event}"))
1753    }
1754
1755    /// The labels a caller opens with land on the opening's envelope, where
1756    /// a reader finds them without knowing what `session_opened` records.
1757    ///
1758    /// This is what lets many runs share one log: they are told apart by a
1759    /// label, not by being given a database each.
1760    #[tokio::test]
1761    async fn opening_carries_the_labels_it_was_opened_with() {
1762        let meta = obj(json!({ "run": "r-1", "attempt": 2, "retried": true }));
1763        let s = Session::new(ANON.to_string(), None, Some(meta.clone()), &Logs::new())
1764            .await
1765            .expect("open");
1766
1767        let events = s.events(0, usize::MAX).await.expect("events");
1768        assert_eq!(events[0].kind(), KIND_SESSION_OPENED);
1769        assert_eq!(
1770            events[0].get(crate::knl::event::FIELD_META),
1771            Some(&Value::Object(meta)),
1772            "the labels ride the envelope, verbatim"
1773        );
1774        assert_eq!(
1775            data_field(&events[0], "run"),
1776            None,
1777            "a label is not a field of the kind: data stays the kernel's"
1778        );
1779    }
1780
1781    /// An opening nobody labelled carries no label — an empty `meta`, which
1782    /// is what the store gives back for a column nothing was written to. A
1783    /// reader selecting on a label finds nothing rather than an empty string,
1784    /// and an empty table passed in is the same as passing none.
1785    #[tokio::test]
1786    async fn an_unlabelled_opening_carries_no_labels() {
1787        for session in [
1788            new_session(None).await,
1789            Session::new(ANON.to_string(), None, Some(Map::new()), &Logs::new())
1790                .await
1791                .expect("open"),
1792        ] {
1793            let events = session.events(0, usize::MAX).await.expect("events");
1794            let meta = events[0]
1795                .get(crate::knl::event::FIELD_META)
1796                .and_then(Value::as_object)
1797                .expect("the store gives every event a meta object");
1798            assert!(meta.is_empty(), "nothing was named, so nothing is recorded");
1799        }
1800    }
1801
1802    #[tokio::test]
1803    async fn a_new_session_already_carries_session_opened() {
1804        let s = new_session(None).await;
1805        assert_eq!(s.len().await.expect("len"), 1);
1806        let events = s.events(0, usize::MAX).await.expect("events");
1807        assert_eq!(events[0].kind(), KIND_SESSION_OPENED);
1808        assert_eq!(events[0].seq(), 1);
1809        assert!(!s.is_closed());
1810        assert!(!s.id().is_empty());
1811    }
1812
1813    /// The scope is issued when the session opens, and it is not the
1814    /// session: two ids, both real, and neither taken from a caller.  Two
1815    /// sessions are two scopes.
1816    #[tokio::test]
1817    async fn open_issues_a_scope_id_distinct_from_the_session_id() {
1818        let a = new_session(None).await;
1819        let b = new_session(None).await;
1820
1821        assert!(!a.scope_id().is_empty(), "a session opens under a scope");
1822        assert!(!a.id().is_empty());
1823        assert_ne!(
1824            a.scope_id(),
1825            a.id(),
1826            "the scope id names the authority, the session id names the stream"
1827        );
1828        assert_eq!(a.scope().id(), a.scope_id(), "the delegate reads the scope");
1829        assert_eq!(a.scope().owner(), a.owner(), "and so does the owner");
1830
1831        assert_ne!(a.scope_id(), b.scope_id(), "two sessions, two scopes");
1832        assert_ne!(a.id(), b.id());
1833    }
1834
1835    /// The scope is in the log, not only in the value: it rides on the
1836    /// session's opening and on every entry of the ledger, so a reader can
1837    /// tell whose authority each move of the balance was made under.
1838    #[tokio::test]
1839    async fn session_opened_and_every_budget_event_carry_the_scope_id() {
1840        let mut s = new_session(Some(100)).await;
1841        assert_eq!(s.reserve(30).await, Ok(true));
1842        s.spend(10).await.expect("spend");
1843        assert_eq!(s.reserve(10_000).await, Ok(false));
1844
1845        let scope_id = s.scope_id().to_string();
1846        let events = s.events(0, usize::MAX).await.expect("events");
1847
1848        let opened = &events[0];
1849        assert_eq!(opened.kind(), KIND_SESSION_OPENED);
1850        assert_eq!(
1851            field(opened, FIELD_SCOPE_ID).as_str(),
1852            Some(scope_id.as_str()),
1853            "the scope rides on session_opened: {opened}"
1854        );
1855        assert_eq!(
1856            field(opened, FIELD_OWNER).as_str(),
1857            Some(ANON),
1858            "beside the owner: {opened}"
1859        );
1860
1861        let moves = ledger(&s).await;
1862        assert_eq!(
1863            kinds(&moves),
1864            vec![
1865                KIND_BUDGET_GRANTED,
1866                KIND_BUDGET_RESERVED,
1867                KIND_BUDGET_SPENT,
1868                KIND_BUDGET_REFUSED,
1869            ],
1870            "every kind of move is exercised"
1871        );
1872        for event in &moves {
1873            assert_eq!(
1874                field(event, FIELD_SCOPE_ID).as_str(),
1875                Some(scope_id.as_str()),
1876                "a ledger entry must name the scope it was allowed under: {event}"
1877            );
1878        }
1879
1880        // An event a caller appends carries no scope id: the field is the
1881        // kernel's, on the kinds only the kernel writes.
1882        s.append(obj(json!({ "kind": "note" })))
1883            .await
1884            .expect("append");
1885        let note = s
1886            .events(0, usize::MAX)
1887            .await
1888            .expect("events")
1889            .pop()
1890            .expect("note");
1891        assert_eq!(data_field(&note, FIELD_SCOPE_ID), None, "{note}");
1892        assert_eq!(note[FIELD_DATA], json!({}), "and no data of its own");
1893    }
1894
1895    #[tokio::test]
1896    async fn the_owner_is_total_and_read_back_verbatim() {
1897        assert_eq!(new_session(None).await.owner(), ANON);
1898        assert_eq!(
1899            Session::new(SYSTEM.to_string(), None, None, &Logs::new())
1900                .await
1901                .expect("open")
1902                .owner(),
1903            SYSTEM
1904        );
1905        assert_eq!(
1906            Session::new("user-42".to_string(), None, None, &Logs::new())
1907                .await
1908                .expect("open")
1909                .owner(),
1910            "user-42"
1911        );
1912    }
1913
1914    #[tokio::test]
1915    async fn close_records_session_closed_once_with_the_given_reason() {
1916        let mut s = new_session(None).await;
1917        s.close(Some("budget_exhausted")).await.expect("close");
1918        s.close(Some("ignored")).await.expect("close (idempotent)");
1919        assert_eq!(s.len().await.expect("len"), 2, "close must be idempotent");
1920
1921        let last = s
1922            .events(2, usize::MAX)
1923            .await
1924            .expect("events")
1925            .pop()
1926            .expect("session_closed");
1927        assert_eq!(last.kind(), KIND_SESSION_CLOSED);
1928        assert_eq!(*field(&last, FIELD_REASON), json!("budget_exhausted"));
1929        assert!(s.is_closed());
1930    }
1931
1932    #[tokio::test]
1933    async fn close_without_a_reason_records_the_default() {
1934        let mut s = new_session(None).await;
1935        s.close(None).await.expect("close");
1936        let last = s
1937            .events(2, usize::MAX)
1938            .await
1939            .expect("events")
1940            .pop()
1941            .expect("session_closed");
1942        assert_eq!(*field(&last, FIELD_REASON), json!(DEFAULT_CLOSE_REASON));
1943    }
1944
1945    /// The detached close is the same boundary, written without waiting: the
1946    /// backstop's path, exercised here on the store that can take it.
1947    #[tokio::test]
1948    async fn a_detached_close_records_the_same_boundary() {
1949        let mut s = new_session(None).await;
1950        s.close_detached(CLOSE_REASON_DROPPED);
1951        assert!(s.is_closed(), "the handle is closed straight away");
1952        // …and closing again writes nothing, exactly as the awaited path.
1953        s.close_detached(CLOSE_REASON_DROPPED);
1954        s.close(Some("ignored")).await.expect("close is a no-op");
1955
1956        // Nothing was awaited above, so the read below is what waits: the
1957        // connection runs one job at a time in the order it took them, so a
1958        // read submitted after the detached write is answered after it.
1959        let last = s
1960            .events(0, usize::MAX)
1961            .await
1962            .expect("events")
1963            .pop()
1964            .expect("session_closed");
1965        assert_eq!(last.kind(), KIND_SESSION_CLOSED);
1966        assert_eq!(
1967            *field(&last, FIELD_REASON),
1968            json!(CLOSE_REASON_DROPPED),
1969            "exactly one boundary, carrying the backstop's reason"
1970        );
1971        assert_eq!(s.len().await.expect("len"), 2, "session_opened + closed");
1972    }
1973
1974    #[tokio::test]
1975    async fn a_closed_session_rejects_writes_but_keeps_serving_reads() {
1976        let mut s = new_session(Some(10)).await;
1977        s.append(obj(json!({ "kind": "note" })))
1978            .await
1979            .expect("append");
1980        s.spend(4).await.expect("spend");
1981        s.close(None).await.expect("close");
1982
1983        let err = s
1984            .append(obj(json!({ "kind": "note" })))
1985            .await
1986            .expect_err("append after close");
1987        assert_eq!(err.reason(), "session is closed");
1988        let err = s.spend(1).await.expect_err("spend after close");
1989        assert_eq!(err.reason(), "session is closed");
1990        let err = s.reserve(1).await.expect_err("reserve after close");
1991        assert_eq!(err.reason(), "session is closed");
1992
1993        assert_eq!(
1994            s.len().await.expect("len"),
1995            5,
1996            "session_opened + budget_granted + note + budget_spent + session_closed"
1997        );
1998        assert_eq!(remaining(&s).await, Some(6));
1999        assert_eq!(
2000            folded(&s).await,
2001            remaining(&s).await,
2002            "the ledger is the balance"
2003        );
2004        assert!(!exhausted(&s).await);
2005        assert_eq!(
2006            s.events(0, usize::MAX).await.expect("events")[2].kind(),
2007            "note"
2008        );
2009    }
2010
2011    #[tokio::test]
2012    async fn two_sessions_share_nothing() {
2013        let mut a = new_session(Some(100)).await;
2014        let mut b = new_session(Some(100)).await;
2015        assert_ne!(a.id(), b.id());
2016
2017        a.append(obj(json!({ "kind": "only_in_a" })))
2018            .await
2019            .expect("append");
2020        a.spend(60).await.expect("spend");
2021
2022        assert_eq!(
2023            a.len().await.expect("len"),
2024            4,
2025            "session_opened + budget_granted + only_in_a + budget_spent"
2026        );
2027        assert_eq!(
2028            b.len().await.expect("len"),
2029            2,
2030            "session_opened + budget_granted"
2031        );
2032        assert_eq!(remaining(&a).await, Some(40));
2033        assert_eq!(remaining(&b).await, Some(100));
2034        // The ledgers are as separate as the histories.
2035        assert_eq!(folded(&a).await, Some(40));
2036        assert_eq!(folded(&b).await, Some(100));
2037
2038        a.close(None).await.expect("close");
2039        assert!(b.append(obj(json!({ "kind": "still_open" }))).await.is_ok());
2040    }
2041
2042    #[tokio::test]
2043    async fn view_serves_the_one_named_projection_and_rejects_anything_else() {
2044        let mut s = new_session(None).await;
2045        s.append(obj(
2046            json!({ "kind": "msg_user", "data": { "content": "hi" } }),
2047        ))
2048        .await
2049        .expect("append");
2050        s.append(response(9)).await.expect("recorded");
2051
2052        let tail = s
2053            .view(VIEW_TAIL, Some(&obj(json!({ "n": 1 }))))
2054            .await
2055            .expect("tail");
2056        assert_eq!(tail.as_array().map(Vec::len), Some(1));
2057
2058        let err = s.view("nope", None).await.expect_err("unknown view");
2059        assert_eq!(err.reason(), r#"unknown view "nope""#);
2060    }
2061
2062    /// A store that records what each read was asked for and what it handed
2063    /// back, so a test can say how much of a stream a view actually touched.
2064    ///
2065    /// Every read a session takes goes through one of these two methods, so
2066    /// the counters are the whole of what the session asked the backend for.
2067    #[derive(Default)]
2068    struct CountingStore {
2069        inner: MemEventStore,
2070        /// `(from_seq, limit)` of every range read, in order.
2071        ranges: Arc<Mutex<Vec<(u64, usize)>>>,
2072        /// `(n, rows handed back)` of every read from the end.
2073        tails: Arc<Mutex<Vec<(usize, usize)>>>,
2074    }
2075
2076    #[async_trait::async_trait]
2077    impl EventStore for CountingStore {
2078        async fn append(&mut self, event: Map<String, Value>) -> KnlResult<crate::knl::Committed> {
2079            self.inner.append(event).await
2080        }
2081
2082        async fn append_if(
2083            &mut self,
2084            kinds: Option<&[&str]>,
2085            decide: crate::knl::Decision,
2086        ) -> KnlResult<Option<crate::knl::Committed>> {
2087            self.inner.append_if(kinds, decide).await
2088        }
2089
2090        async fn read_kinds(
2091            &self,
2092            kinds: Option<&[&str]>,
2093            from_seq: u64,
2094            limit: usize,
2095        ) -> KnlResult<Vec<Value>> {
2096            self.ranges
2097                .lock()
2098                .unwrap_or_else(PoisonError::into_inner)
2099                .push((from_seq, limit));
2100            self.inner.read_kinds(kinds, from_seq, limit).await
2101        }
2102
2103        async fn read_last(&self, n: usize) -> KnlResult<Vec<Value>> {
2104            let events = self.inner.read_last(n).await?;
2105            self.tails
2106                .lock()
2107                .unwrap_or_else(PoisonError::into_inner)
2108                .push((n, events.len()));
2109            Ok(events)
2110        }
2111
2112        async fn head(&self) -> KnlResult<Option<u64>> {
2113            self.inner.head().await
2114        }
2115
2116        async fn len(&self) -> KnlResult<usize> {
2117            self.inner.len().await
2118        }
2119    }
2120
2121    /// `tail` asks the store for the end of the stream, not for the stream.
2122    ///
2123    /// The regression this pins: the view used to read every event, upcast
2124    /// every one of them and then keep the last `n`, so a five-event answer
2125    /// off a long log cost the whole log.  A thousand events in, a `tail(5)`
2126    /// must reach the backend as "the last five" and come back as five —
2127    /// and no range read may go out behind it asking for everything.
2128    #[tokio::test]
2129    async fn tail_reads_the_end_of_the_stream_and_not_the_whole_of_it() {
2130        let store = CountingStore::default();
2131        let (ranges, tails) = (Arc::clone(&store.ranges), Arc::clone(&store.tails));
2132        let mut s = Session::open_on(ANON.to_string(), None, None, Box::new(store))
2133            .await
2134            .expect("open");
2135        for i in 0..1_000 {
2136            s.append(obj(json!({ "kind": format!("e{i}") })))
2137                .await
2138                .expect("append");
2139        }
2140
2141        // The opening plus the thousand: what a whole read would cost.
2142        assert_eq!(s.len().await.expect("len"), 1_001);
2143        ranges
2144            .lock()
2145            .unwrap_or_else(PoisonError::into_inner)
2146            .clear();
2147        tails.lock().unwrap_or_else(PoisonError::into_inner).clear();
2148
2149        let tail = s
2150            .view(VIEW_TAIL, Some(&obj(json!({ "n": 5 }))))
2151            .await
2152            .expect("tail");
2153        let tail = tail.as_array().expect("an array of events");
2154        assert_eq!(tail.len(), 5);
2155        assert_eq!(kind_of(&tail[4]), "e999", "the last event is the last one");
2156
2157        // One read, from the end, for exactly the five that were asked for.
2158        let taken = tails.lock().unwrap_or_else(PoisonError::into_inner).clone();
2159        let scanned = ranges
2160            .lock()
2161            .unwrap_or_else(PoisonError::into_inner)
2162            .clone();
2163        assert_eq!(
2164            taken,
2165            vec![(5, 5)],
2166            "tail must ask the store for five rows and get five"
2167        );
2168        assert!(
2169            scanned.is_empty(),
2170            "no range read goes out behind it: {scanned:?}"
2171        );
2172    }
2173
2174    /// The token account is not a name the kernel answers to any more: it
2175    /// reads the `llm_response` payload, so it is a query view written over
2176    /// the published schema.  Asking the kernel for it is the same error as
2177    /// asking for any other name it does not have.
2178    #[tokio::test]
2179    async fn the_token_account_is_not_a_named_view() {
2180        let mut s = new_session(None).await;
2181        s.append(response(9)).await.expect("recorded");
2182
2183        let err = s.view("usage", None).await.expect_err("usage was served");
2184        assert_eq!(err.reason(), r#"unknown view "usage""#);
2185        assert_eq!(err.kind(), KnlError::VALIDATION);
2186
2187        // What it needs is in the log, verbatim, for a reader to sum.
2188        let recorded = s
2189            .events(2, usize::MAX)
2190            .await
2191            .expect("events")
2192            .pop()
2193            .expect("llm_response");
2194        assert_eq!(*field(&recorded, "usage"), json!({ "input_tokens": 9 }));
2195    }
2196
2197    /// The conversation is not one of the names: how a record becomes a
2198    /// request — which role each kind takes, whether a system message
2199    /// belongs in it, where to cut it off — is the shell's decision, and
2200    /// it builds it from `events` rather than asking the kernel for it.
2201    #[tokio::test]
2202    async fn the_conversation_is_not_a_named_view() {
2203        let mut s = new_session(None).await;
2204        s.append(obj(
2205            json!({ "kind": "msg_user", "data": { "content": "hi" } }),
2206        ))
2207        .await
2208        .expect("append");
2209
2210        let err = s
2211            .view("dialogue", None)
2212            .await
2213            .expect_err("dialogue was served");
2214        assert_eq!(err.reason(), r#"unknown view "dialogue""#);
2215
2216        let events = s.events(0, usize::MAX).await.expect("events");
2217        assert_eq!(events[1].kind(), "msg_user");
2218        assert_eq!(*field(&events[1], "content"), json!("hi"));
2219    }
2220
2221    /// Appending an `llm_response` records it — verbatim, beat included —
2222    /// and leaves the budget alone.  What a call was allowed to cost was
2223    /// decided before it happened; the record of it happening is not a
2224    /// second place where that is decided.
2225    #[tokio::test]
2226    async fn appending_an_llm_response_records_it_verbatim_without_charging() {
2227        let mut s = new_session(Some(100)).await;
2228        let seq = s
2229            .append(obj(json!({
2230                "kind": "llm_response",
2231                // The beat is the caller's word and the kernel keeps it.
2232                "meta": { "beat": "beat-7" },
2233                "data": {
2234                    "content": [{ "type": "text", "text": "hi" }],
2235                    "usage": { "input_tokens": 20, "output_tokens": 10 }
2236                }
2237            })))
2238            .await
2239            .expect("append");
2240
2241        let recorded = s
2242            .events(seq, usize::MAX)
2243            .await
2244            .expect("events")
2245            .pop()
2246            .expect("llm_response");
2247        assert_eq!(recorded.kind(), "llm_response");
2248        assert_eq!(
2249            recorded[FIELD_META][FIELD_BEAT],
2250            json!("beat-7"),
2251            "the declared beat is recorded as given"
2252        );
2253
2254        assert_eq!(remaining(&s).await, Some(100), "an append must not charge");
2255        assert_eq!(
2256            ledger(&s).await.len(),
2257            1,
2258            "only the opening grant is in the ledger"
2259        );
2260        assert_eq!(
2261            *field(&recorded, "usage"),
2262            json!({ "input_tokens": 20, "output_tokens": 10 }),
2263            "the counts are stored as they came: {recorded}"
2264        );
2265        assert_eq!(
2266            folded(&s).await,
2267            Some(100),
2268            "what was consumed and the balance are separate readings"
2269        );
2270    }
2271
2272    /// The grant is the first thing the ledger says, right after the
2273    /// session's own boundary, with the owner's words on it.
2274    #[tokio::test]
2275    async fn opening_with_a_grant_records_it() {
2276        let s = Session::new(
2277            ANON.to_string(),
2278            Some(BudgetGrant {
2279                amount: 500,
2280                tag: Some("tokens".to_string()),
2281                desc: Some("one nightly run".to_string()),
2282            }),
2283            None,
2284            &Logs::new(),
2285        )
2286        .await
2287        .expect("open");
2288
2289        let events = s.events(0, usize::MAX).await.expect("events");
2290        assert_eq!(events.len(), 2, "session_opened + budget_granted");
2291        assert_eq!(events[0].kind(), KIND_SESSION_OPENED);
2292        assert_eq!(
2293            data_field(&events[0], "budget"),
2294            None,
2295            "the grant is its own event, not a field on session_opened"
2296        );
2297
2298        let granted = &events[1];
2299        assert_eq!(granted.kind(), KIND_BUDGET_GRANTED);
2300        assert_eq!(*field(granted, FIELD_AMOUNT), json!(500));
2301        assert_eq!(*field(granted, FIELD_TAG), json!("tokens"));
2302        assert_eq!(*field(granted, FIELD_DESC), json!("one nightly run"));
2303        assert_eq!(remaining(&s).await, Some(500));
2304        assert_eq!(folded(&s).await, remaining(&s).await);
2305
2306        // A session with no grant keeps no ledger at all.
2307        let bare = new_session(None).await;
2308        assert_eq!(bare.len().await.expect("len"), 1, "session_opened only");
2309        assert!(ledger(&bare).await.is_empty());
2310        assert_eq!(folded(&bare).await, None);
2311    }
2312
2313    /// A reservation the balance covers is recorded once, deducts exactly
2314    /// what it asked for, and carries the grant's tag.
2315    #[tokio::test]
2316    async fn a_granted_reservation_is_one_event_and_one_deduction() {
2317        let mut s = new_session(Some(100)).await;
2318        assert_eq!(s.reserve(30).await, Ok(true));
2319
2320        let moves = ledger(&s).await;
2321        assert_eq!(moves.len(), 2, "the grant and the reservation");
2322        assert_eq!(moves[1].kind(), KIND_BUDGET_RESERVED);
2323        assert_eq!(*field(&moves[1], FIELD_AMOUNT), json!(30));
2324        assert_eq!(*field(&moves[1], FIELD_TAG), json!("tokens"));
2325        assert_eq!(remaining(&s).await, Some(70));
2326        assert_eq!(
2327            folded(&s).await,
2328            remaining(&s).await,
2329            "the ledger is the balance"
2330        );
2331    }
2332
2333    /// A refusal is a fact: it is recorded, with what was asked for and
2334    /// what there was, and it moves nothing.
2335    #[tokio::test]
2336    async fn a_refused_reservation_is_recorded_and_changes_no_balance() {
2337        let mut s = new_session(Some(10)).await;
2338        assert_eq!(s.reserve(11).await, Ok(false));
2339
2340        let moves = ledger(&s).await;
2341        assert_eq!(moves.len(), 2, "the grant and the refusal");
2342        assert_eq!(moves[1].kind(), KIND_BUDGET_REFUSED);
2343        assert_eq!(*field(&moves[1], FIELD_AMOUNT), json!(11));
2344        assert_eq!(
2345            *field(&moves[1], FIELD_REMAINING),
2346            json!(10),
2347            "what there was"
2348        );
2349        assert_eq!(*field(&moves[1], FIELD_TAG), json!("tokens"));
2350        assert_eq!(remaining(&s).await, Some(10), "a refusal must not deduct");
2351        assert!(!exhausted(&s).await);
2352        assert_eq!(folded(&s).await, remaining(&s).await);
2353
2354        // And the run can still spend what it has: nothing was consumed.
2355        assert_eq!(s.reserve(10).await, Ok(true));
2356        assert_eq!(remaining(&s).await, Some(0));
2357        assert_eq!(folded(&s).await, Some(0));
2358    }
2359
2360    /// The settlement is recorded like everything else, and what the session
2361    /// reports is the fold of the ledger after any sequence of moves.
2362    #[tokio::test]
2363    async fn the_balance_is_the_fold_after_any_sequence_of_moves() {
2364        let mut s = new_session(Some(1000)).await;
2365        assert_eq!(s.reserve(200).await, Ok(true));
2366        s.append(response(40)).await.expect("recorded");
2367        s.spend(50).await.expect("spend");
2368        assert_eq!(s.reserve(10_000).await, Ok(false));
2369        assert_eq!(s.reserve(300).await, Ok(true));
2370        s.spend(0).await.expect("spend");
2371
2372        let moves = ledger(&s).await;
2373        assert_eq!(
2374            kinds(&moves),
2375            vec![
2376                KIND_BUDGET_GRANTED,
2377                KIND_BUDGET_RESERVED,
2378                KIND_BUDGET_SPENT,
2379                KIND_BUDGET_REFUSED,
2380                KIND_BUDGET_RESERVED,
2381                KIND_BUDGET_SPENT,
2382            ],
2383            "every move left exactly one event"
2384        );
2385        assert_eq!(remaining(&s).await, Some(450), "1000 - 200 - 50 - 300");
2386        assert_eq!(folded(&s).await, remaining(&s).await);
2387    }
2388
2389    /// The ledger is the kernel's to write: a caller cannot grant itself a
2390    /// budget, or drain one, by appending the events the balance folds
2391    /// from.
2392    #[tokio::test]
2393    async fn a_caller_cannot_append_the_budget_kinds() {
2394        let mut s = new_session(Some(10)).await;
2395        for event in [
2396            json!({ "kind": "budget_granted", "data": { "amount": 1_000_000 } }),
2397            json!({ "kind": "budget_reserved", "data": { "amount": 5 } }),
2398            json!({ "kind": "budget_refused", "data": { "amount": 5, "remaining": 0 } }),
2399            json!({ "kind": "budget_spent", "data": { "amount": 5 } }),
2400        ] {
2401            let err = s
2402                .append(obj(event.clone()))
2403                .await
2404                .expect_err("kernel-only kind");
2405            assert!(
2406                err.reason().contains("kernel only"),
2407                "{event}: {}",
2408                err.reason()
2409            );
2410        }
2411
2412        assert_eq!(
2413            remaining(&s).await,
2414            Some(10),
2415            "no forged event moved the balance"
2416        );
2417        assert_eq!(ledger(&s).await.len(), 1, "nothing was recorded");
2418        assert_eq!(folded(&s).await, remaining(&s).await);
2419    }
2420
2421    /// The session's boundaries are the kernel's alone: a caller cannot
2422    /// hand-append either one, so a stream cannot claim an opening it never
2423    /// had or an ending it never reached.  The refusal leaves the session
2424    /// open and the log untouched.
2425    #[tokio::test]
2426    async fn a_caller_cannot_append_the_session_boundary_kinds() {
2427        let mut s = new_session(Some(100)).await;
2428        for event in [
2429            json!({ "kind": "session_opened", "data": { "scope_id": "s", "owner": "me" } }),
2430            json!({ "kind": "session_closed", "data": { "reason": "carried over" } }),
2431        ] {
2432            let err = s
2433                .append(obj(event.clone()))
2434                .await
2435                .expect_err("kernel-only kind");
2436            assert!(
2437                err.reason().contains("kernel only"),
2438                "{event}: {}",
2439                err.reason()
2440            );
2441        }
2442
2443        assert!(!s.is_closed(), "a refused append ended the session");
2444        assert_eq!(s.len().await.expect("len"), 2, "nothing was recorded");
2445        assert_eq!(s.append(obj(json!({ "kind": "note" }))).await, Ok(3));
2446        assert_eq!(s.spend(10).await, Ok(()));
2447        assert_eq!(remaining(&s).await, Some(90), "the settlement landed");
2448    }
2449
2450    /// Only `close` records `session_closed`, and it records exactly one:
2451    /// the flag and the event move together, so the log and the state
2452    /// cannot disagree.
2453    #[tokio::test]
2454    async fn only_close_records_session_closed() {
2455        let mut s = new_session(Some(100)).await;
2456        s.append(obj(json!({ "kind": "note" })))
2457            .await
2458            .expect("append");
2459        assert!(
2460            !s.events(0, usize::MAX)
2461                .await
2462                .expect("events")
2463                .iter()
2464                .any(|e| e.kind() == KIND_SESSION_CLOSED),
2465            "nothing but close writes the boundary"
2466        );
2467
2468        s.close(Some("done")).await.expect("close");
2469        assert!(s.is_closed());
2470
2471        let closed: Vec<Current> = s
2472            .events(0, usize::MAX)
2473            .await
2474            .expect("events")
2475            .into_iter()
2476            .filter(|e| e.kind() == KIND_SESSION_CLOSED)
2477            .collect();
2478        assert_eq!(closed.len(), 1, "exactly one boundary: {closed:?}");
2479        assert_eq!(*field(&closed[0], FIELD_REASON), json!("done"));
2480        assert_eq!(
2481            s.append(obj(json!({ "kind": "note" })))
2482                .await
2483                .expect_err("append after close")
2484                .reason(),
2485            "session is closed"
2486        );
2487    }
2488
2489    /// The record and the account are separate readings of the same
2490    /// session: the response is in the history, counts and all, and the
2491    /// balance is exactly what was granted, because nobody reserved
2492    /// anything.
2493    #[tokio::test]
2494    async fn a_recorded_response_is_in_the_history_without_being_charged() {
2495        let mut s = new_session(Some(100)).await;
2496        s.append(response(30)).await.expect("recorded");
2497
2498        assert_eq!(remaining(&s).await, Some(100));
2499        assert!(!exhausted(&s).await);
2500
2501        let recorded = s
2502            .events(3, usize::MAX)
2503            .await
2504            .expect("events")
2505            .pop()
2506            .expect("llm_response");
2507        assert_eq!(recorded.kind(), "llm_response");
2508        assert_eq!(*field(&recorded, "stop_reason"), json!("end_turn"));
2509        assert_eq!(field(&recorded, "usage")["input_tokens"], json!(30));
2510        assert_eq!(
2511            folded(&s).await,
2512            Some(100),
2513            "the ledger recorded no consumption"
2514        );
2515    }
2516
2517    /// The beat belongs to the layer above: the kernel never mints one, so
2518    /// an event that declares none carries none, and one that declares a
2519    /// `meta.beat` carries exactly the string it was given — on any kind, and
2520    /// repeated across the facts of one beat without the kernel objecting.
2521    #[tokio::test]
2522    async fn beats_are_the_callers_word_and_the_kernel_adds_none() {
2523        let mut s = new_session(None).await;
2524
2525        let seq = s.append(response(1)).await.expect("an undeclared beat");
2526        let bare = s
2527            .events(seq, usize::MAX)
2528            .await
2529            .expect("events")
2530            .pop()
2531            .expect("response");
2532        assert_eq!(
2533            bare[FIELD_META].get(FIELD_BEAT),
2534            None,
2535            "the kernel must not invent a beat: {bare}"
2536        );
2537
2538        for event in [
2539            json!({
2540                "kind": "llm_response", "meta": { "beat": "b-1" },
2541                "data": { "content": [], "usage": { "input_tokens": 1 } }
2542            }),
2543            json!({
2544                "kind": "tool_call", "meta": { "beat": "b-1" },
2545                "data": { "call_id": "c1", "name": "sh", "args": {} }
2546            }),
2547            json!({
2548                "kind": "tool_result", "meta": { "beat": "b-1" },
2549                "data": { "call_id": "c1", "ok": true, "result": "ok" }
2550            }),
2551            json!({
2552                "kind": "llm_call_failed", "meta": { "beat": "b-1" },
2553                "data": { "error": "boom" }
2554            }),
2555        ] {
2556            let seq = s.append(obj(event.clone())).await.expect("declared beat");
2557            let recorded = s
2558                .events(seq, usize::MAX)
2559                .await
2560                .expect("events")
2561                .pop()
2562                .expect("recorded");
2563            assert_eq!(recorded[FIELD_META][FIELD_BEAT], json!("b-1"), "{event}");
2564        }
2565
2566        // The beat is a label now, so writing one at the top level is a stray
2567        // key — and the refusal says where it goes.
2568        let err = s
2569            .append(obj(json!({ "kind": "note", "beat": "b-1" })))
2570            .await
2571            .expect_err("a beat at the top level");
2572        assert!(err.reason().contains("meta.beat"), "{err}");
2573    }
2574
2575    #[tokio::test]
2576    async fn a_closed_session_records_nothing() {
2577        let mut s = new_session(Some(100)).await;
2578        s.close(None).await.expect("close");
2579
2580        let err = s.append(response(10)).await.expect_err("closed session");
2581        assert_eq!(err.reason(), "session is closed");
2582
2583        assert_eq!(
2584            s.len().await.expect("len"),
2585            3,
2586            "session_opened + budget_granted + session_closed only"
2587        );
2588        assert_eq!(remaining(&s).await, Some(100), "nothing was consumed");
2589    }
2590
2591    /// The budget stops a session *before* it spends, not after: a
2592    /// reservation the balance cannot cover is refused, and the call it was
2593    /// for never happens.  This replaces the old contract, where the budget
2594    /// was a flag that only stood up once a recorded call had already used
2595    /// the allowance up — by which time the spending was done.
2596    #[tokio::test]
2597    async fn the_budget_refuses_before_the_call_rather_than_flagging_after_it() {
2598        let mut s = new_session(Some(10)).await;
2599
2600        /// How many model responses the log holds.
2601        async fn responses(s: &Session) -> usize {
2602            s.events(0, usize::MAX)
2603                .await
2604                .expect("events")
2605                .iter()
2606                .filter(|e| e.kind() == "llm_response")
2607                .count()
2608        }
2609
2610        // The estimate fits, so the beat proceeds and records its response.
2611        assert_eq!(s.reserve(10).await, Ok(true));
2612        s.append(response(25)).await.expect("recorded");
2613        assert_eq!(remaining(&s).await, Some(0), "the reservation took it all");
2614        assert!(exhausted(&s).await);
2615
2616        // The next beat asks first and is turned away, so no second
2617        // response is recorded: the caller never made the call.
2618        assert_eq!(s.reserve(1).await, Ok(false));
2619        assert_eq!(responses(&s).await, 1, "the refused beat made no call");
2620        assert_eq!(remaining(&s).await, Some(0));
2621        assert_eq!(folded(&s).await, remaining(&s).await);
2622
2623        // The kernel still does not police it: a caller that ignores the
2624        // refusal can append anyway, and the history says that it did.
2625        s.append(response(5)).await.expect("recorded");
2626        assert_eq!(responses(&s).await, 2, "stopping is the caller's decision");
2627    }
2628
2629    #[tokio::test]
2630    async fn without_a_budget_a_call_reports_no_remaining_and_is_never_exhausted() {
2631        let mut s = new_session(None).await;
2632        s.append(response(9_000)).await.expect("recorded");
2633        assert_eq!(remaining(&s).await, None);
2634        assert!(!exhausted(&s).await);
2635
2636        // No budget, no ledger: reserve always grants, spend does nothing,
2637        // and neither leaves a trace.
2638        assert_eq!(s.reserve(1_000_000).await, Ok(true));
2639        assert_eq!(s.spend(1_000_000).await, Ok(()));
2640        assert!(
2641            ledger(&s).await.is_empty(),
2642            "a run with no quota keeps no ledger"
2643        );
2644        assert_eq!(remaining(&s).await, None);
2645        assert!(!exhausted(&s).await);
2646    }
2647
2648    #[tokio::test]
2649    async fn views_stay_readable_and_correct_after_close() {
2650        let mut s = new_session(None).await;
2651        s.append(response(9)).await.expect("recorded");
2652        let before = s
2653            .view(VIEW_TAIL, Some(&obj(json!({ "n": 1 }))))
2654            .await
2655            .expect("tail");
2656        s.close(None).await.expect("close");
2657        let after = s
2658            .view(VIEW_TAIL, Some(&obj(json!({ "n": 1 }))))
2659            .await
2660            .expect("tail after close");
2661
2662        // The read keeps working, and it reads the log as it now stands: the
2663        // ending this handle wrote is the last thing in it.
2664        assert_eq!(
2665            kind_of(&before.as_array().expect("array")[0]),
2666            "llm_response"
2667        );
2668        assert_eq!(
2669            kind_of(&after.as_array().expect("array")[0]),
2670            KIND_SESSION_CLOSED
2671        );
2672
2673        assert_eq!(s.len().await.expect("len"), 3);
2674        assert_eq!(
2675            s.events(0, usize::MAX).await.expect("events")[2].kind(),
2676            KIND_SESSION_CLOSED
2677        );
2678    }
2679
2680    /// `open_on` on a durable backend records the session's owner on the
2681    /// `session_opened` boundary, so resume can recover it from the log
2682    /// alone.
2683    #[tokio::test]
2684    async fn open_on_records_the_owner_on_session_opened() {
2685        use crate::knl::SqliteEventStore;
2686
2687        let store = SqliteEventStore::open_memory("owner-stream", &Logs::new())
2688            .await
2689            .expect("open");
2690        let s = Session::open_on(
2691            "user-7".to_string(),
2692            Some(grant(100)),
2693            None,
2694            Box::new(store),
2695        )
2696        .await
2697        .expect("open");
2698
2699        let events = s.events(0, usize::MAX).await.expect("events");
2700        let opened = events.first().expect("session_opened");
2701        assert_eq!(opened.kind(), KIND_SESSION_OPENED);
2702        assert_eq!(
2703            field(opened, FIELD_OWNER).as_str(),
2704            Some("user-7"),
2705            "owner rides on session_opened: {opened}"
2706        );
2707        assert_eq!(s.owner(), "user-7");
2708
2709        // The grant is durable too, as its own event.
2710        assert_eq!(events[1].kind(), KIND_BUDGET_GRANTED);
2711        assert_eq!(*field(&events[1], FIELD_AMOUNT), json!(100));
2712    }
2713
2714    /// Resume re-folds a persisted SQLite stream: the owner and the
2715    /// *balance* come back from the log, because every move of the balance
2716    /// is in it.
2717    #[tokio::test]
2718    async fn resume_restores_the_owner_and_the_folded_balance() {
2719        use crate::knl::SqliteEventStore;
2720
2721        let dir = tempfile::tempdir().expect("tempdir");
2722        let path = dir.path().join("events.db");
2723        let stream = "resume-stream";
2724        let logs = Logs::new();
2725
2726        // A durable session: two beats that reserved and settled.
2727        let before_close = {
2728            let store = SqliteEventStore::open(&path, stream, &logs)
2729                .await
2730                .expect("open");
2731            let mut s = Session::open_on(
2732                "user-42".to_string(),
2733                Some(grant(100)),
2734                None,
2735                Box::new(store),
2736            )
2737            .await
2738            .expect("open");
2739            assert_eq!(s.reserve(30).await, Ok(true));
2740            s.append(response(30)).await.expect("first response");
2741            s.append(obj(
2742                json!({ "kind": "msg_user", "data": { "content": "more" } }),
2743            ))
2744            .await
2745            .expect("msg_user");
2746            assert_eq!(s.reserve(15).await, Ok(true));
2747            s.append(response(20)).await.expect("second response");
2748            s.spend(5)
2749                .await
2750                .expect("the second call overran its estimate");
2751            assert_eq!(remaining(&s).await, Some(50), "100 - 30 - 15 - 5");
2752            assert_eq!(folded(&s).await, remaining(&s).await);
2753            remaining(&s).await
2754        }; // dropped: the handle goes, the log persists.
2755
2756        // Reopen the same stream and resume — no new session_opened is
2757        // written, and no new grant either.
2758        let store = SqliteEventStore::open(&path, stream, &logs)
2759            .await
2760            .expect("reopen");
2761        let mut resumed = Session::resume(None, Box::new(store))
2762            .await
2763            .expect("resume");
2764
2765        assert_eq!(
2766            resumed.owner(),
2767            "user-42",
2768            "owner restored from session_opened"
2769        );
2770        assert_eq!(
2771            remaining(&resumed).await,
2772            before_close,
2773            "the balance is what the ledger says it was"
2774        );
2775        assert_eq!(
2776            resumed.grant().and_then(|g| g.tag.as_deref()),
2777            Some("tokens"),
2778            "the grant's words came back with it"
2779        );
2780        // Resume appended nothing: the log is exactly what was persisted.
2781        assert_eq!(
2782            resumed.len().await.expect("len"),
2783            8,
2784            "session_opened + granted + reserved + response + msg_user \
2785             + reserved + response + spent — and nothing from resume itself"
2786        );
2787
2788        // The record came back whole, so a reader that sums the counts —
2789        // the Lua query view, over the published schema — has both calls to
2790        // work from.
2791        let responses: Vec<Current> = resumed
2792            .events(0, usize::MAX)
2793            .await
2794            .expect("events")
2795            .into_iter()
2796            .filter(|e| e.kind() == "llm_response")
2797            .collect();
2798        assert_eq!(responses.len(), 2, "{responses:?}");
2799        assert_eq!(field(&responses[0], "usage")["input_tokens"], json!(30));
2800        assert_eq!(field(&responses[1], "usage")["input_tokens"], json!(20));
2801
2802        // The ledger continues: the next reservation comes off the restored
2803        // balance, and what the resumed session records is its own.
2804        assert_eq!(resumed.reserve(5).await, Ok(true));
2805        let seq = resumed.append(response(5)).await.expect("third response");
2806        let recorded = resumed
2807            .events(seq, usize::MAX)
2808            .await
2809            .expect("events")
2810            .pop()
2811            .expect("llm_response");
2812        assert_eq!(recorded.kind(), "llm_response");
2813        assert_eq!(remaining(&resumed).await, Some(45), "5 reserved off the 50");
2814        assert_eq!(folded(&resumed).await, remaining(&resumed).await);
2815    }
2816
2817    /// A `grant` on resume is the owner allowing *more*: it is recorded and
2818    /// added to what the log left, rather than replacing it.
2819    #[tokio::test]
2820    async fn resume_with_a_grant_records_it_and_raises_the_balance() {
2821        use crate::knl::SqliteEventStore;
2822
2823        let dir = tempfile::tempdir().expect("tempdir");
2824        let path = dir.path().join("events.db");
2825        let stream = "regrant-stream";
2826        let logs = Logs::new();
2827
2828        {
2829            let store = SqliteEventStore::open(&path, stream, &logs)
2830                .await
2831                .expect("open");
2832            let mut s = Session::open_on(
2833                "user-9".to_string(),
2834                Some(grant(100)),
2835                None,
2836                Box::new(store),
2837            )
2838            .await
2839            .expect("open");
2840            assert_eq!(s.reserve(80).await, Ok(true));
2841            assert_eq!(remaining(&s).await, Some(20));
2842        }
2843
2844        let store = SqliteEventStore::open(&path, stream, &logs)
2845            .await
2846            .expect("reopen");
2847        let mut resumed = Session::resume(
2848            Some(BudgetGrant {
2849                amount: 50,
2850                tag: Some("tokens".to_string()),
2851                desc: Some("a little more".to_string()),
2852            }),
2853            Box::new(store),
2854        )
2855        .await
2856        .expect("resume");
2857
2858        assert_eq!(remaining(&resumed).await, Some(70), "20 left + 50 granted");
2859        assert_eq!(folded(&resumed).await, remaining(&resumed).await);
2860
2861        let moves = ledger(&resumed).await;
2862        assert_eq!(
2863            kinds(&moves),
2864            vec![
2865                KIND_BUDGET_GRANTED,
2866                KIND_BUDGET_RESERVED,
2867                KIND_BUDGET_GRANTED
2868            ],
2869            "the second grant is a recorded fact"
2870        );
2871        assert_eq!(*field(&moves[2], FIELD_AMOUNT), json!(50));
2872        assert_eq!(*field(&moves[2], FIELD_DESC), json!("a little more"));
2873
2874        // And the resumed run spends against the raised balance.
2875        assert_eq!(resumed.reserve(70).await, Ok(true));
2876        assert_eq!(resumed.reserve(1).await, Ok(false));
2877        assert_eq!(remaining(&resumed).await, Some(0));
2878        assert_eq!(folded(&resumed).await, remaining(&resumed).await);
2879    }
2880
2881    /// One session, one budget: a resume raises a ledger that exists and
2882    /// cannot start one that does not.
2883    ///
2884    /// The two-handle case this rules out: a session opens with no quota, so
2885    /// its handle refuses nothing and its caller was told there is nothing to
2886    /// refuse; a second handle resumes the same open stream with a grant, and
2887    /// from the next reservation on the first handle is bounded by an
2888    /// allowance nobody asked it about.  Whether a session has a budget is
2889    /// settled when it opens.
2890    #[tokio::test]
2891    async fn a_resume_does_not_give_a_stream_the_budget_it_opened_without() {
2892        use crate::knl::SqliteEventStore;
2893
2894        let dir = tempfile::tempdir().expect("tempdir");
2895        let path = dir.path().join("events.db");
2896        let stream = "ungranted-stream";
2897        let logs = Logs::new();
2898
2899        // Opened with no budget, and still open: the handle is held for the
2900        // whole test, so nothing has written an ending.
2901        let store = SqliteEventStore::open(&path, stream, &logs)
2902            .await
2903            .expect("open");
2904        let first = Session::open_on("user-1".to_string(), None, None, Box::new(store))
2905            .await
2906            .expect("open");
2907        assert_eq!(
2908            first.len().await.expect("len"),
2909            1,
2910            "session_opened, and no grant beside it"
2911        );
2912
2913        let reopened = SqliteEventStore::open(&path, stream, &logs)
2914            .await
2915            .expect("reopen");
2916        let err = Session::resume(Some(grant(100)), Box::new(reopened))
2917            .await
2918            .expect_err("a resume must not introduce a ledger");
2919        assert_eq!(
2920            err.kind(),
2921            KnlError::VALIDATION,
2922            "the caller's argument is what did not hold up: {err}"
2923        );
2924        assert!(
2925            err.reason().contains("opened with no budget"),
2926            "{}",
2927            err.reason()
2928        );
2929
2930        // Nothing was written for it: the refusal is decided in the same
2931        // transaction that would have written the grant.
2932        assert_eq!(
2933            first.len().await.expect("len"),
2934            1,
2935            "the stream is untouched"
2936        );
2937        assert_eq!(remaining(&first).await, None, "and still has no ledger");
2938
2939        // Resuming it *without* a grant is what a second handle does.
2940        let reopened = SqliteEventStore::open(&path, stream, &logs)
2941            .await
2942            .expect("reopen");
2943        let second = Session::resume(None, Box::new(reopened))
2944            .await
2945            .expect("a resume with no grant is the ordinary one");
2946        assert_eq!(remaining(&second).await, None);
2947        assert_eq!(second.len().await.expect("len"), 1);
2948    }
2949
2950    /// Whether there is a budget is the *log's* answer, not the handle's.
2951    ///
2952    /// A handle opened without a grant used to short-circuit on its own
2953    /// cached scope: `reserve` answered `true` whatever the ledger said and
2954    /// `spend` wrote nothing, so a grant that reached the stream through
2955    /// another handle was invisible to it and the quota bounded nobody.  The
2956    /// question is inside the decision now, so the grant in the log binds
2957    /// every handle on the stream.
2958    #[tokio::test]
2959    async fn a_handle_opened_without_a_grant_is_bound_by_the_grant_the_log_carries() {
2960        use crate::knl::SqliteEventStore;
2961
2962        let dir = tempfile::tempdir().expect("tempdir");
2963        let path = dir.path().join("events.db");
2964        let stream = "late-grant-stream";
2965        let logs = Logs::new();
2966
2967        let store = SqliteEventStore::open(&path, stream, &logs)
2968            .await
2969            .expect("open");
2970        let mut opened_without =
2971            Session::open_on("user-1".to_string(), None, None, Box::new(store))
2972                .await
2973                .expect("open");
2974
2975        // The owner grants, through a handle it holds on the same stream.
2976        let reopened = SqliteEventStore::open(&path, stream, &logs)
2977            .await
2978            .expect("reopen");
2979        let mut owner_handle = Session::resume(None, Box::new(reopened))
2980            .await
2981            .expect("resume");
2982        owner_handle
2983            .grant_more(grant(100))
2984            .await
2985            .expect("the owner grants");
2986
2987        // The first handle's own scope still says there is no budget…
2988        assert_eq!(
2989            opened_without.grant(),
2990            None,
2991            "the cached grant is a hint, and this handle never got one"
2992        );
2993        // …and the ledger it is measured against is the log's.
2994        assert_eq!(
2995            opened_without.reserve(500).await,
2996            Ok(false),
2997            "500 does not fit in the 100 the log carries"
2998        );
2999        assert_eq!(opened_without.reserve(40).await, Ok(true));
3000        assert_eq!(remaining(&opened_without).await, Some(60));
3001        opened_without
3002            .spend(60)
3003            .await
3004            .expect("a deduction on a ledger this handle did not open");
3005        assert_eq!(remaining(&opened_without).await, Some(0));
3006        assert_eq!(
3007            folded(&opened_without).await,
3008            remaining(&opened_without).await
3009        );
3010
3011        // Every entry it wrote is tagged with the unit the *log's* grant
3012        // named, not with the nothing this handle was opened with.
3013        let moves = ledger(&opened_without).await;
3014        assert_eq!(
3015            kinds(&moves),
3016            vec![
3017                KIND_BUDGET_GRANTED,
3018                KIND_BUDGET_REFUSED,
3019                KIND_BUDGET_RESERVED,
3020                KIND_BUDGET_SPENT,
3021            ],
3022        );
3023        for event in &moves {
3024            assert_eq!(
3025                field(event, FIELD_TAG).as_str(),
3026                Some("tokens"),
3027                "the unit comes off the log's grant: {event}"
3028            );
3029        }
3030        assert_eq!(*field(&moves[1], FIELD_REMAINING), json!(100));
3031    }
3032
3033    /// Seed `stream` with a `session_opened` whose `data` is empty.
3034    ///
3035    /// The validator requires the scope on that kind, so this cannot be
3036    /// written through the store: the row goes in behind it, which is what a
3037    /// stream an upcaster could not bring all the way would look like.  The
3038    /// resume fallbacks below are for exactly that, and this is the only way
3039    /// to reach them.
3040    async fn seed_an_opening_with_no_scope(path: &std::path::Path, stream: &str) {
3041        use crate::knl::SqliteEventStore;
3042
3043        // Open once so the table is there, then write past the validator.
3044        // The collection is shut down rather than dropped, so the connection
3045        // has actually finished before the direct write below.
3046        let logs = Logs::new();
3047        drop(
3048            SqliteEventStore::open(path, stream, &logs)
3049                .await
3050                .expect("open"),
3051        );
3052        assert!(logs.shutdown().await.is_empty(), "the writer joined");
3053        let conn = rusqlite::Connection::open(path).expect("open the database directly");
3054        conn.execute(
3055            "INSERT INTO events \
3056             (stream, seq, epoch_ms, kind, schema_version, meta, data) \
3057             VALUES (?1, 1, 0, ?2, 2, '{}', '{}')",
3058            rusqlite::params![stream, KIND_SESSION_OPENED],
3059        )
3060        .expect("seed the opening");
3061        // The stream's next `seq` is a stored counter, not `MAX(seq)` — which
3062        // is what stops a number being handed out twice after history is
3063        // removed — so a row written behind the store has to move it, or the
3064        // next real append would be numbered 1 again.
3065        conn.execute(
3066            "INSERT INTO stream_seq (stream, next_seq) VALUES (?1, 2) \
3067             ON CONFLICT(stream) DO UPDATE SET next_seq = excluded.next_seq",
3068            rusqlite::params![stream],
3069        )
3070        .expect("seed the counter");
3071    }
3072
3073    /// A log whose `session_opened` carries no `owner` resumes as [`ANON`]
3074    /// rather than failing: a stream that arrives missing the field is still
3075    /// a session, and refusing it would lose the log rather than protect it.
3076    #[tokio::test]
3077    async fn resume_falls_back_to_anon_when_the_log_has_no_owner() {
3078        use crate::knl::SqliteEventStore;
3079
3080        let dir = tempfile::tempdir().expect("tempdir");
3081        let path = dir.path().join("events.db");
3082        let stream = "legacy-stream";
3083        seed_an_opening_with_no_scope(&path, stream).await;
3084
3085        let store = SqliteEventStore::open(&path, stream, &Logs::new())
3086            .await
3087            .expect("reopen");
3088        let resumed = Session::resume(None, Box::new(store))
3089            .await
3090            .expect("resume");
3091        assert_eq!(resumed.owner(), ANON);
3092        assert_eq!(
3093            remaining(&resumed).await,
3094            None,
3095            "resumed without a budget cap"
3096        );
3097    }
3098
3099    /// Resume restores the *scope*, not just a fresh one: the id and the
3100    /// owner come back off `session_opened`, so the session continues under
3101    /// the authority the log says it opened with, and the ledger it goes on
3102    /// writing names that same scope.
3103    #[tokio::test]
3104    async fn resume_restores_the_scope_id_and_owner_from_the_log() {
3105        use crate::knl::SqliteEventStore;
3106
3107        let dir = tempfile::tempdir().expect("tempdir");
3108        let path = dir.path().join("events.db");
3109        let stream = "scope-resume-stream";
3110        let logs = Logs::new();
3111
3112        let opened_scope = {
3113            let store = SqliteEventStore::open(&path, stream, &logs)
3114                .await
3115                .expect("open");
3116            let mut s = Session::open_on(
3117                "user-11".to_string(),
3118                Some(grant(100)),
3119                None,
3120                Box::new(store),
3121            )
3122            .await
3123            .expect("open");
3124            assert_eq!(s.reserve(40).await, Ok(true));
3125            s.scope_id().to_string()
3126        };
3127
3128        let store = SqliteEventStore::open(&path, stream, &logs)
3129            .await
3130            .expect("reopen");
3131        let mut resumed = Session::resume(None, Box::new(store))
3132            .await
3133            .expect("resume");
3134        assert_eq!(
3135            resumed.scope_id(),
3136            opened_scope,
3137            "the scope id is restored from session_opened, not re-issued"
3138        );
3139        assert_eq!(resumed.owner(), "user-11");
3140        assert_eq!(resumed.scope().owner(), "user-11");
3141        assert_eq!(
3142            remaining(&resumed).await,
3143            Some(60),
3144            "the balance is the fold's"
3145        );
3146
3147        // What the resumed session records goes on naming the same scope.
3148        assert_eq!(resumed.reserve(10).await, Ok(true));
3149        let last = ledger(&resumed).await.pop().expect("budget_reserved");
3150        assert_eq!(last.kind(), KIND_BUDGET_RESERVED);
3151        assert_eq!(
3152            field(&last, FIELD_SCOPE_ID).as_str(),
3153            Some(opened_scope.as_str()),
3154            "{last}"
3155        );
3156    }
3157
3158    /// A log whose `session_opened` carries no `scope_id` resumes under a
3159    /// fresh kernel-issued one rather than failing — the sibling of the
3160    /// `owner` fallback above, and for the same reason.
3161    #[tokio::test]
3162    async fn resume_issues_a_fresh_scope_id_when_the_log_records_none() {
3163        use crate::knl::SqliteEventStore;
3164
3165        let dir = tempfile::tempdir().expect("tempdir");
3166        let path = dir.path().join("events.db");
3167        let stream = "legacy-scope-stream";
3168        seed_an_opening_with_no_scope(&path, stream).await;
3169
3170        let store = SqliteEventStore::open(&path, stream, &Logs::new())
3171            .await
3172            .expect("reopen");
3173        // Resumed with no grant, because a resume cannot give a stream one it
3174        // opened without ([`Session::grant_on_resume`]); the owner grants
3175        // through the handle below, which is what puts a `budget_granted` in
3176        // the log for the scope id to be read off.
3177        let mut resumed = Session::resume(None, Box::new(store))
3178            .await
3179            .expect("resume");
3180        resumed
3181            .grant_more(grant(50))
3182            .await
3183            .expect("the owner grants");
3184
3185        // The fallback is visible from both sides: the log says nothing…
3186        let opened = resumed
3187            .events(0, usize::MAX)
3188            .await
3189            .expect("events")
3190            .remove(0);
3191        assert_eq!(opened.kind(), KIND_SESSION_OPENED);
3192        assert_eq!(data_field(&opened, FIELD_SCOPE_ID), None, "{opened}");
3193        assert_eq!(data_field(&opened, FIELD_OWNER), None, "{opened}");
3194        // …and the resumed session has a real scope all the same.
3195        assert!(
3196            !resumed.scope_id().is_empty(),
3197            "an older log must still resume under a scope"
3198        );
3199        assert_eq!(resumed.owner(), ANON, "the sibling fallback");
3200
3201        // And it is the one everything written from here on names.
3202        let granted = ledger(&resumed).await.pop().expect("budget_granted");
3203        assert_eq!(granted.kind(), KIND_BUDGET_GRANTED);
3204        assert_eq!(
3205            field(&granted, FIELD_SCOPE_ID).as_str(),
3206            Some(resumed.scope_id()),
3207            "{granted}"
3208        );
3209    }
3210
3211    /// (Fix 5) Resuming an empty log is a caller error — a mistyped or
3212    /// nonexistent stream must not fold into an anonymous zero session.
3213    #[tokio::test]
3214    async fn resume_of_an_empty_store_is_a_caller_error_not_an_anon_session() {
3215        let err = Session::resume(Some(grant(100)), Box::new(MemEventStore::new()))
3216            .await
3217            .expect_err("an empty store has no session to resume");
3218        assert!(
3219            err.reason().contains("no session to resume"),
3220            "{}",
3221            err.reason()
3222        );
3223    }
3224
3225    /// (Fix 5) A log that has events but no opening the kernel recognises —
3226    /// under any shape it has ever been written in — is a caller error too:
3227    /// the ANON fallback is only for a real `session_opened`.
3228    #[tokio::test]
3229    async fn resume_of_a_store_without_an_opening_is_a_caller_error() {
3230        let mut store = MemEventStore::new();
3231        store
3232            .append(obj(json!({ "kind": "note" })))
3233            .await
3234            .expect("seed a non-opening event");
3235        let err = Session::resume(None, Box::new(store))
3236            .await
3237            .expect_err("a log with no opening has no session to resume");
3238        assert!(
3239            err.reason().contains("no session to resume"),
3240            "{}",
3241            err.reason()
3242        );
3243    }
3244
3245    /// A session is disposable: once its ending is in the log, the stream is
3246    /// not continued.  What comes after an ending is a new session.
3247    #[tokio::test]
3248    async fn a_closed_stream_is_not_resumed() {
3249        let mut store = MemEventStore::new();
3250        store
3251            .append(obj(json!({
3252                "kind": "session_opened",
3253                "data": { "scope_id": "scope-5", "owner": "user-5" }
3254            })))
3255            .await
3256            .expect("seed the opening");
3257        store
3258            .append(obj(
3259                json!({ "kind": "budget_granted", "data": { "amount": 100 } }),
3260            ))
3261            .await
3262            .expect("seed the grant");
3263        store
3264            .append(obj(
3265                json!({ "kind": "session_closed", "data": { "reason": "done" } }),
3266            ))
3267            .await
3268            .expect("seed the ending");
3269
3270        let err = Session::resume(None, Box::new(store))
3271            .await
3272            .expect_err("a closed session must not be resumed");
3273        assert!(
3274            err.reason().contains("session is closed"),
3275            "{}",
3276            err.reason()
3277        );
3278        assert!(err.reason().contains("disposable"), "{}", err.reason());
3279    }
3280
3281    /// A competing writer lands an event between this session's writes, so
3282    /// the log has moved on at the moment this one appends.  The append still
3283    /// lands — a fact is not refused for what its writer had seen — and the
3284    /// `seq` it comes back with is where it really landed, after whatever got
3285    /// in first.
3286    struct BusyStore {
3287        inner: MemEventStore,
3288        injected: bool,
3289    }
3290
3291    #[async_trait::async_trait]
3292    impl EventStore for BusyStore {
3293        /// A session's appends come through here, so this is where the
3294        /// competing writer gets in: once, just before the response this
3295        /// session is about to record.
3296        async fn append(&mut self, event: Map<String, Value>) -> KnlResult<crate::knl::Committed> {
3297            if !self.injected
3298                && event.get(FIELD_KIND).and_then(Value::as_str) == Some("llm_response")
3299            {
3300                self.injected = true;
3301                self.inner
3302                    .append(obj(json!({ "kind": "sneaked_in" })))
3303                    .await
3304                    .expect("injected concurrent write");
3305            }
3306            self.inner.append(event).await
3307        }
3308
3309        async fn append_if(
3310            &mut self,
3311            kinds: Option<&[&str]>,
3312            decide: crate::knl::Decision,
3313        ) -> KnlResult<Option<crate::knl::Committed>> {
3314            self.inner.append_if(kinds, decide).await
3315        }
3316
3317        async fn read_kinds(
3318            &self,
3319            kinds: Option<&[&str]>,
3320            from_seq: u64,
3321            limit: usize,
3322        ) -> KnlResult<Vec<Value>> {
3323            self.inner.read_kinds(kinds, from_seq, limit).await
3324        }
3325
3326        async fn head(&self) -> KnlResult<Option<u64>> {
3327            self.inner.head().await
3328        }
3329
3330        async fn len(&self) -> KnlResult<usize> {
3331            self.inner.len().await
3332        }
3333    }
3334
3335    /// An append records a fact and the store orders it: another writer
3336    /// getting there first does not turn this session's append into a
3337    /// failure, it only decides where the two land.
3338    #[tokio::test]
3339    async fn an_append_lands_after_a_competing_write_rather_than_being_refused() {
3340        let store = BusyStore {
3341            inner: MemEventStore::new(),
3342            injected: false,
3343        };
3344        let mut s = Session::open_on("user".to_string(), Some(grant(1000)), None, Box::new(store))
3345            .await
3346            .expect("open");
3347        assert_eq!(
3348            s.len().await.expect("len"),
3349            2,
3350            "session_opened + budget_granted so far"
3351        );
3352
3353        // The competing write lands at seq 3, so the response lands at 4 —
3354        // and it lands.
3355        let seq = s
3356            .append(response(10))
3357            .await
3358            .expect("an append is not refused");
3359        assert_eq!(seq, 4, "the seq is where the event really landed");
3360        assert_eq!(s.len().await.expect("len"), 4, "both writes are in the log");
3361
3362        let log = s.events(0, usize::MAX).await.expect("events");
3363        assert_eq!(
3364            kinds(&log),
3365            [
3366                KIND_SESSION_OPENED,
3367                KIND_BUDGET_GRANTED,
3368                "sneaked_in",
3369                "llm_response"
3370            ],
3371            "the log interleaves in arrival order"
3372        );
3373        assert_eq!(
3374            remaining(&s).await,
3375            Some(1000),
3376            "an append still charges nothing"
3377        );
3378    }
3379
3380    /// A store that takes a decision's write and refuses a plain one, from
3381    /// the moment the test arms it.
3382    ///
3383    /// The two paths a `budget_*` event could reach the log by, told apart:
3384    /// what a decision returns goes in with the read it was decided against
3385    /// ([`EventStore::append_if`]), and anything else is a second write. A
3386    /// refusal that came out here as a plain append would fail on this store
3387    /// while the decision that produced it had already succeeded — which is
3388    /// precisely the state a caller cannot read back.
3389    struct DecidedWritesOnlyStore {
3390        inner: MemEventStore,
3391        armed: Arc<AtomicBool>,
3392    }
3393
3394    #[async_trait::async_trait]
3395    impl EventStore for DecidedWritesOnlyStore {
3396        async fn append(&mut self, event: Map<String, Value>) -> KnlResult<crate::knl::Committed> {
3397            if self.armed.load(Ordering::Relaxed) {
3398                return Err(KnlError::Storage(
3399                    "this store takes only what a decision wrote".to_string(),
3400                ));
3401            }
3402            self.inner.append(event).await
3403        }
3404
3405        async fn append_if(
3406            &mut self,
3407            kinds: Option<&[&str]>,
3408            decide: crate::knl::Decision,
3409        ) -> KnlResult<Option<crate::knl::Committed>> {
3410            self.inner.append_if(kinds, decide).await
3411        }
3412
3413        async fn read_kinds(
3414            &self,
3415            kinds: Option<&[&str]>,
3416            from_seq: u64,
3417            limit: usize,
3418        ) -> KnlResult<Vec<Value>> {
3419            self.inner.read_kinds(kinds, from_seq, limit).await
3420        }
3421
3422        async fn head(&self) -> KnlResult<Option<u64>> {
3423            self.inner.head().await
3424        }
3425
3426        async fn len(&self) -> KnlResult<usize> {
3427            self.inner.len().await
3428        }
3429    }
3430
3431    /// A refusal is written by the decision that refused, not by a second
3432    /// append afterwards.
3433    ///
3434    /// It used to be the second append, and that made two different outcomes
3435    /// look the same: if the write of the `budget_refused` failed, the caller
3436    /// got a storage error with nothing in the log to say the reservation had
3437    /// been decided at all — indistinguishable from a decision that never
3438    /// happened. Now exactly one of `budget_reserved` / `budget_refused`
3439    /// lands, in the transaction that took the decision, and the answer this
3440    /// call gives is which of the two it was.
3441    #[tokio::test]
3442    async fn a_refusal_lands_in_the_transaction_that_decided_it() {
3443        let armed = Arc::new(AtomicBool::new(false));
3444        let store = DecidedWritesOnlyStore {
3445            inner: MemEventStore::new(),
3446            armed: Arc::clone(&armed),
3447        };
3448        let mut s = Session::open_on("user".to_string(), Some(grant(10)), None, Box::new(store))
3449            .await
3450            .expect("open");
3451
3452        // From here on, a plain append fails: only a decision may write.
3453        armed.store(true, Ordering::Relaxed);
3454
3455        assert_eq!(
3456            s.reserve(50).await,
3457            Ok(false),
3458            "the refusal is the decision's own write, so it lands"
3459        );
3460        assert_eq!(
3461            s.reserve(4).await,
3462            Ok(true),
3463            "and so is the reservation that fits"
3464        );
3465
3466        let moves = ledger(&s).await;
3467        assert_eq!(
3468            kinds(&moves),
3469            vec![
3470                KIND_BUDGET_GRANTED,
3471                KIND_BUDGET_REFUSED,
3472                KIND_BUDGET_RESERVED
3473            ],
3474            "exactly one entry per decision"
3475        );
3476        assert_eq!(*field(&moves[1], FIELD_AMOUNT), json!(50), "what was asked");
3477        assert_eq!(
3478            *field(&moves[1], FIELD_REMAINING),
3479            json!(10),
3480            "and the balance the decision measured it against"
3481        );
3482        assert_eq!(remaining(&s).await, Some(6), "a refusal moved nothing");
3483
3484        // The store really is refusing plain appends: an ordinary record is
3485        // the thing this session can no longer write.
3486        let err = s
3487            .append(obj(json!({ "kind": "note" })))
3488            .await
3489            .expect_err("a plain append");
3490        assert_eq!(err.kind(), KnlError::STORAGE);
3491    }
3492
3493    /// A store whose `head` read is down: appends land, but nothing can ask
3494    /// the ledger where it stands.
3495    struct HeadlessStore {
3496        inner: MemEventStore,
3497    }
3498
3499    #[async_trait::async_trait]
3500    impl EventStore for HeadlessStore {
3501        async fn append(&mut self, event: Map<String, Value>) -> KnlResult<crate::knl::Committed> {
3502            self.inner.append(event).await
3503        }
3504
3505        async fn append_if(
3506            &mut self,
3507            kinds: Option<&[&str]>,
3508            decide: crate::knl::Decision,
3509        ) -> KnlResult<Option<crate::knl::Committed>> {
3510            self.inner.append_if(kinds, decide).await
3511        }
3512
3513        async fn read_kinds(
3514            &self,
3515            kinds: Option<&[&str]>,
3516            from_seq: u64,
3517            limit: usize,
3518        ) -> KnlResult<Vec<Value>> {
3519            self.inner.read_kinds(kinds, from_seq, limit).await
3520        }
3521
3522        async fn head(&self) -> KnlResult<Option<u64>> {
3523            Err(KnlError::Busy("the head read is contended".to_string()))
3524        }
3525
3526        async fn len(&self) -> KnlResult<usize> {
3527            self.inner.len().await
3528        }
3529    }
3530
3531    /// A store that cannot be read has no balance to report, and the kernel
3532    /// says so rather than serving the last fold.
3533    ///
3534    /// Both values this call can otherwise hand back read as facts about the
3535    /// budget — a number says "you have this much", a `false` from
3536    /// `exhausted` says "carry on" — and the caller acting on either is a
3537    /// loop deciding whether it may go on spending.  So the failure
3538    /// surfaces, classified, and what to do about a contended read is the
3539    /// caller's.
3540    #[tokio::test]
3541    async fn a_balance_that_cannot_be_read_is_an_error_not_a_stale_fold() {
3542        let store = HeadlessStore {
3543            inner: MemEventStore::new(),
3544        };
3545        let s = Session::open_on("user".to_string(), Some(grant(100)), None, Box::new(store))
3546            .await
3547            .expect("the appends land; only the head read is down");
3548
3549        let err = s
3550            .remaining()
3551            .await
3552            .expect_err("a failed read must not fold into a number");
3553        assert_eq!(err.kind(), KnlError::BUSY, "the class travels out intact");
3554        assert!(err.is_retryable(), "contention is the one retryable class");
3555
3556        let err = s.exhausted().await.expect_err("nor into a boolean");
3557        assert_eq!(err.kind(), KnlError::BUSY);
3558
3559        // Only the reading failed: the record itself is exactly as written.
3560        assert_eq!(
3561            s.len().await.expect("len"),
3562            2,
3563            "session_opened + budget_granted landed"
3564        );
3565    }
3566
3567    /// (Fix 5) Resuming a nonexistent SQLite stream is a caller error, not an
3568    /// anonymous empty session.
3569    #[tokio::test]
3570    async fn resume_of_a_nonexistent_sqlite_stream_is_a_caller_error() {
3571        use crate::knl::SqliteEventStore;
3572
3573        let dir = tempfile::tempdir().expect("tempdir");
3574        let path = dir.path().join("events.db");
3575        // A stream that was never opened as a session: its log is empty.
3576        let store = SqliteEventStore::open(&path, "ghost-stream", &Logs::new())
3577            .await
3578            .expect("open");
3579        let err = Session::resume(Some(grant(100)), Box::new(store))
3580            .await
3581            .expect_err("an empty stream has no session to resume");
3582        assert!(
3583            err.reason().contains("no session to resume"),
3584            "{}",
3585            err.reason()
3586        );
3587    }
3588
3589    /// (Concurrency) Two `Session` handles on ONE durable stream, each with a
3590    /// view of the head from before the other wrote: both append, and the log
3591    /// holds both in the order they arrived.  This is the scenario that used
3592    /// to be a head conflict — an append records a fact, and a fact is not
3593    /// refused for what its writer had last seen.
3594    #[tokio::test]
3595    async fn two_sessions_on_one_stream_both_append_and_the_log_interleaves() {
3596        use crate::knl::SqliteEventStore;
3597
3598        let dir = tempfile::tempdir().expect("tempdir");
3599        let path = dir.path().join("events.db");
3600        let stream = "interleave-stream";
3601        let logs = Logs::new();
3602
3603        // A opens the session on the shared stream: `session_opened` at seq 1
3604        // and its `budget_granted` at seq 2, so A has seen head 2.
3605        let store_a = SqliteEventStore::open(&path, stream, &logs)
3606            .await
3607            .expect("open A");
3608        let mut a = Session::open_on(
3609            "user".to_string(),
3610            Some(grant(1000)),
3611            None,
3612            Box::new(store_a),
3613        )
3614        .await
3615        .expect("open A");
3616
3617        // B resumes the SAME stream while it holds only those two, so both
3618        // handles have seen exactly head 2.  (It resumes before A closes: a
3619        // closed session is not resumable.)
3620        let store_b = SqliteEventStore::open(&path, stream, &logs)
3621            .await
3622            .expect("open B");
3623        let mut b = Session::resume(None, Box::new(store_b))
3624            .await
3625            .expect("resume B");
3626        assert_eq!(remaining(&b).await, Some(1000), "B resumed on A's ledger");
3627        assert_eq!(
3628            (a.len().await.expect("len"), b.len().await.expect("len")),
3629            (2, 2),
3630            "both see the same two events"
3631        );
3632
3633        // A appends, so B's view is now out of date — and B appends anyway.
3634        assert_eq!(a.append(response(10)).await.expect("A appends"), 3);
3635        assert_eq!(
3636            b.append(response(20)).await.expect("B appends too"),
3637            4,
3638            "B's write lands after A's, rather than being refused"
3639        );
3640        // And A, now out of date in its turn, goes on writing.
3641        assert_eq!(a.append(response(30)).await.expect("A appends again"), 5);
3642
3643        // The durable log holds all three, in arrival order.
3644        let verify = SqliteEventStore::open(&path, stream, &logs)
3645            .await
3646            .expect("reopen to verify");
3647        let log = as_current(verify.read(0, usize::MAX).await.expect("read log"));
3648        let responses: Vec<u64> = log
3649            .iter()
3650            .filter(|e| e.kind() == "llm_response")
3651            .map(Current::seq)
3652            .collect();
3653        assert_eq!(responses, [3, 4, 5], "every append landed, in order");
3654    }
3655
3656    /// (Concurrency) The invariant that *is* a decision: two handles on one
3657    /// stream, ten granted, each asking for six.  The decision is taken inside
3658    /// the store, against the ledger as it stands there, so exactly one is
3659    /// allowed — and the fold says four, not minus two.
3660    #[tokio::test]
3661    async fn two_sessions_cannot_both_reserve_the_same_allowance() {
3662        use crate::knl::SqliteEventStore;
3663
3664        let dir = tempfile::tempdir().expect("tempdir");
3665        let path = dir.path().join("events.db");
3666        let stream = "reserve-race-stream";
3667        let logs = Logs::new();
3668
3669        let store_a = SqliteEventStore::open(&path, stream, &logs)
3670            .await
3671            .expect("open A");
3672        let mut a = Session::open_on("user".to_string(), Some(grant(10)), None, Box::new(store_a))
3673            .await
3674            .expect("open A");
3675        let store_b = SqliteEventStore::open(&path, stream, &logs)
3676            .await
3677            .expect("open B");
3678        let mut b = Session::resume(None, Box::new(store_b))
3679            .await
3680            .expect("resume B");
3681        assert_eq!(remaining(&b).await, Some(10), "both see the whole grant");
3682        assert_eq!(remaining(&a).await, Some(10));
3683
3684        // A takes six.  B still believes it has ten — and is refused all the
3685        // same, because the balance it is measured against is the one in the
3686        // store, not the one it cached.
3687        assert_eq!(a.reserve(6).await, Ok(true), "the first reservation fits");
3688        assert_eq!(b.reserve(6).await, Ok(false), "the second does not");
3689        assert_eq!(remaining(&b).await, Some(4), "B's balance is the ledger's");
3690        assert_eq!(remaining(&a).await, Some(4), "and so is A's");
3691
3692        // The ledger is the answer: 10 granted − 6 reserved = 4, with the
3693        // refusal recorded and moving nothing.
3694        let verify = SqliteEventStore::open(&path, stream, &logs)
3695            .await
3696            .expect("reopen to verify");
3697        let log = as_current(verify.read(0, usize::MAX).await.expect("read log"));
3698        assert_eq!(fold_balance(&log), Some(4), "no allowance was taken twice");
3699        let moves: Vec<&str> = kinds(&log)
3700            .into_iter()
3701            .filter(|k| k.starts_with("budget_"))
3702            .collect();
3703        assert_eq!(
3704            moves,
3705            [
3706                KIND_BUDGET_GRANTED,
3707                KIND_BUDGET_RESERVED,
3708                KIND_BUDGET_REFUSED
3709            ],
3710            "one grant, one reservation, one refusal"
3711        );
3712        let refused = log.last().expect("the refusal");
3713        assert_eq!(*field(refused, FIELD_AMOUNT), json!(6));
3714        assert_eq!(
3715            *field(refused, FIELD_REMAINING),
3716            json!(4),
3717            "what there really was"
3718        );
3719    }
3720
3721    /// (Concurrency) "Closed" is the handle's, and the log records what
3722    /// arrives after it.  Three handles, one close: the two that never saw it
3723    /// go on writing, and their writes land *after* the `session_closed` —
3724    /// which is the fact an audit is reading for, and would be gone if the
3725    /// store had refused them.  A second handle closing writes a second
3726    /// ending, because that is what happened.
3727    #[tokio::test]
3728    async fn a_close_is_the_handles_and_the_log_records_what_arrives_after_it() {
3729        use crate::knl::SqliteEventStore;
3730
3731        let dir = tempfile::tempdir().expect("tempdir");
3732        let path = dir.path().join("events.db");
3733        let stream = "close-race-stream";
3734        let logs = Logs::new();
3735
3736        let store_a = SqliteEventStore::open(&path, stream, &logs)
3737            .await
3738            .expect("open A");
3739        let mut a = Session::open_on(
3740            "user".to_string(),
3741            Some(grant(100)),
3742            None,
3743            Box::new(store_a),
3744        )
3745        .await
3746        .expect("open A");
3747        // Both resume while the stream is open — a closed one is not
3748        // resumable — so both hold `closed = false` across A's close.
3749        let store_b = SqliteEventStore::open(&path, stream, &logs)
3750            .await
3751            .expect("open B");
3752        let mut b = Session::resume(None, Box::new(store_b))
3753            .await
3754            .expect("resume B");
3755        let store_c = SqliteEventStore::open(&path, stream, &logs)
3756            .await
3757            .expect("open C");
3758        let mut c = Session::resume(None, Box::new(store_c))
3759            .await
3760            .expect("resume C");
3761
3762        a.close(Some("done")).await.expect("A closes");
3763        assert!(a.is_closed());
3764        assert!(!b.is_closed(), "B's flag is its own and has not moved");
3765        assert!(!c.is_closed());
3766
3767        // B writes, and the write lands: the store serializes appends, it does
3768        // not adjudicate them.
3769        assert_eq!(
3770            b.append(obj(json!({ "kind": "note" }))).await,
3771            Ok(4),
3772            "an append after another handle's close is recorded"
3773        );
3774        assert!(!b.is_closed(), "landing a write closed nothing");
3775
3776        // C's budget moves are decided on the balance alone — 100 granted,
3777        // nothing spent, so both go through.
3778        assert_eq!(c.reserve(5).await, Ok(true), "the ledger covers it");
3779        assert_eq!(c.spend(10).await, Ok(()), "the settlement lands");
3780        assert_eq!(
3781            remaining(&c).await,
3782            Some(85),
3783            "100 − 5 − 10, folded in the tx"
3784        );
3785        assert!(!c.is_closed());
3786
3787        // B closing writes a *second* ending; A closing again writes nothing,
3788        // because A's own flag is set.
3789        b.close(Some("late")).await.expect("B closes");
3790        a.close(Some("again"))
3791            .await
3792            .expect("A is idempotent per handle");
3793
3794        let verify = SqliteEventStore::open(&path, stream, &logs)
3795            .await
3796            .expect("reopen to verify");
3797        let log = as_current(verify.read(0, usize::MAX).await.expect("read log"));
3798        assert_eq!(
3799            kinds(&log),
3800            [
3801                KIND_SESSION_OPENED,
3802                KIND_BUDGET_GRANTED,
3803                KIND_SESSION_CLOSED,
3804                "note",
3805                KIND_BUDGET_RESERVED,
3806                KIND_BUDGET_SPENT,
3807                KIND_SESSION_CLOSED,
3808            ],
3809            "everything that happened, in the order it arrived"
3810        );
3811
3812        let endings: Vec<&Current> = log
3813            .iter()
3814            .filter(|event| event.kind() == KIND_SESSION_CLOSED)
3815            .collect();
3816        assert_eq!(endings.len(), 2, "two handles closed, two endings recorded");
3817        assert_eq!(*field(endings[0], FIELD_REASON), json!("done"));
3818        assert_eq!(*field(endings[1], FIELD_REASON), json!("late"));
3819        assert_eq!(
3820            fold_balance(&log),
3821            Some(85),
3822            "the ledger is what the moves that landed add up to"
3823        );
3824    }
3825
3826    /// (Concurrency) A settlement records the move and says nothing else; the
3827    /// balance afterwards is the ledger's, so it is exact on a stream two
3828    /// handles write to — B reads what the log says, not a number it was
3829    /// holding before A spent.
3830    #[tokio::test]
3831    async fn a_settlement_records_the_move_and_the_balance_is_the_ledgers() {
3832        use crate::knl::SqliteEventStore;
3833
3834        let dir = tempfile::tempdir().expect("tempdir");
3835        let path = dir.path().join("events.db");
3836        let stream = "spend-race-stream";
3837        let logs = Logs::new();
3838
3839        let store_a = SqliteEventStore::open(&path, stream, &logs)
3840            .await
3841            .expect("open A");
3842        let mut a = Session::open_on(
3843            "user".to_string(),
3844            Some(grant(100)),
3845            None,
3846            Box::new(store_a),
3847        )
3848        .await
3849        .expect("open A");
3850        let store_b = SqliteEventStore::open(&path, stream, &logs)
3851            .await
3852            .expect("open B");
3853        let mut b = Session::resume(None, Box::new(store_b))
3854            .await
3855            .expect("resume B");
3856        assert_eq!(
3857            (remaining(&a).await, remaining(&b).await),
3858            (Some(100), Some(100))
3859        );
3860
3861        assert_eq!(a.spend(30).await, Ok(()), "A settles 30 of the 100");
3862        assert_eq!(
3863            remaining(&a).await,
3864            Some(70),
3865            "and reads the balance separately"
3866        );
3867        assert_eq!(
3868            remaining(&b).await,
3869            Some(70),
3870            "B wrote nothing and still reads A's settlement off the ledger"
3871        );
3872
3873        // B's own settlement measures against the ledger — 100 − 30 − 20 —
3874        // rather than subtracting 20 from a number it was holding.
3875        assert_eq!(b.spend(20).await, Ok(()), "B settles 20");
3876        assert_eq!(remaining(&b).await, Some(50), "both settlements are in it");
3877
3878        let verify = SqliteEventStore::open(&path, stream, &logs)
3879            .await
3880            .expect("reopen to verify");
3881        let log = as_current(verify.read(0, usize::MAX).await.expect("read log"));
3882        assert_eq!(fold_balance(&log), Some(50), "the balance is the fold");
3883        let moves: Vec<&str> = kinds(&log)
3884            .into_iter()
3885            .filter(|kind| kind.starts_with("budget_"))
3886            .collect();
3887        assert_eq!(
3888            moves,
3889            [KIND_BUDGET_GRANTED, KIND_BUDGET_SPENT, KIND_BUDGET_SPENT],
3890            "one grant and two settlements"
3891        );
3892
3893        // A settlement never refuses: it floors at zero, as the fold does.
3894        assert_eq!(a.spend(1_000).await, Ok(()));
3895        assert_eq!(b.spend(1).await, Ok(()));
3896        assert_eq!(remaining(&a).await, Some(0));
3897        assert_eq!(remaining(&b).await, Some(0));
3898        let verify = SqliteEventStore::open(&path, stream, &logs)
3899            .await
3900            .expect("reopen to verify");
3901        assert_eq!(
3902            fold_balance(&as_current(
3903                verify.read(0, usize::MAX).await.expect("read log")
3904            )),
3905            Some(0),
3906            "the ledger floors at zero rather than going into debt"
3907        );
3908    }
3909
3910    /// (Concurrency) The balance is the ledger and nothing else, so a handle
3911    /// that has written nothing at all still reports what the other one
3912    /// spent: `B` never calls a write in this test, and every answer it gives
3913    /// comes from folding the stream it shares with `A`.
3914    #[tokio::test]
3915    async fn a_handle_that_wrote_nothing_reports_what_the_other_spent() {
3916        use crate::knl::SqliteEventStore;
3917
3918        let dir = tempfile::tempdir().expect("tempdir");
3919        let path = dir.path().join("events.db");
3920        let stream = "shared-balance-stream";
3921        let logs = Logs::new();
3922
3923        let store_a = SqliteEventStore::open(&path, stream, &logs)
3924            .await
3925            .expect("open A");
3926        let mut a = Session::open_on(
3927            "user".to_string(),
3928            Some(grant(100)),
3929            None,
3930            Box::new(store_a),
3931        )
3932        .await
3933        .expect("open A");
3934        let store_b = SqliteEventStore::open(&path, stream, &logs)
3935            .await
3936            .expect("open B");
3937        // Not `mut`: reading a balance is a read, and B does nothing else.
3938        let b = Session::resume(None, Box::new(store_b))
3939            .await
3940            .expect("resume B");
3941        assert_eq!(
3942            remaining(&b).await,
3943            Some(100),
3944            "both start on the same ledger"
3945        );
3946
3947        assert_eq!(a.spend(30).await, Ok(()), "A settles 30");
3948        assert_eq!(
3949            remaining(&b).await,
3950            Some(70),
3951            "B sees the settlement it did not make"
3952        );
3953
3954        assert_eq!(a.reserve(20).await, Ok(true), "A reserves 20");
3955        assert_eq!(remaining(&b).await, Some(50), "and the reservation too");
3956        assert!(!exhausted(&b).await);
3957
3958        // Reading twice over a stream that has not moved repeats the fold's
3959        // answer rather than drifting from it.
3960        assert_eq!(
3961            remaining(&b).await,
3962            Some(50),
3963            "a second read is the same read"
3964        );
3965
3966        assert_eq!(a.spend(1_000).await, Ok(()), "A overspends");
3967        assert_eq!(remaining(&b).await, Some(0), "the floor is the ledger's");
3968        assert!(exhausted(&b).await);
3969
3970        // And the log is the whole of the story: nothing B holds was needed.
3971        let verify = SqliteEventStore::open(&path, stream, &logs)
3972            .await
3973            .expect("reopen to verify");
3974        assert_eq!(
3975            fold_balance(&as_current(
3976                verify.read(0, usize::MAX).await.expect("read log")
3977            )),
3978            remaining(&b).await
3979        );
3980    }
3981
3982    /// A test-local step, standing in for a real one: it renames the two kinds
3983    /// a hypothetical earlier shape used.  The kernel chain is empty until the
3984    /// first release, so the seam is exercised with a chain the test owns.
3985    ///
3986    /// It leaves the version alone.  These rows are written by the store
3987    /// itself, so they already carry [`CURRENT_SCHEMA_VERSION`] — the version
3988    /// a step produces — and a `Current` is asserted to be at it, so a fixture
3989    /// that stamped anything else would be claiming a version the kernel does
3990    /// not have.
3991    struct RenameLegacyKinds;
3992
3993    impl crate::knl::Upcaster for RenameLegacyKinds {
3994        fn upcast(&self, mut event: Value) -> Value {
3995            // Not an object at all: unchanged.  An upcaster is total and
3996            // infallible.
3997            let Some(map) = event.as_object_mut() else {
3998                return event;
3999            };
4000            let renamed = match map.get(FIELD_KIND).and_then(Value::as_str) {
4001                Some("legacy_opened") => Some(KIND_SESSION_OPENED),
4002                Some("legacy_response") => Some("llm_response"),
4003                _ => None,
4004            };
4005            if let Some(kind) = renamed {
4006                map.insert(FIELD_KIND.to_string(), Value::from(kind));
4007            }
4008            event
4009        }
4010    }
4011
4012    /// (Upcasting seam) Every read a session makes goes through the chain
4013    /// wrapped round its backend — the restore fold a resume takes, `events`,
4014    /// the `tail` view and the balance fold alike — while the rows on disk
4015    /// keep the shape they were written in.
4016    #[tokio::test]
4017    async fn a_session_reads_every_path_through_the_upcaster_seam() {
4018        use crate::knl::{
4019            SqliteEventStore, Upcaster, CURRENT_SCHEMA_VERSION, SCHEMA_VERSION_FIELD,
4020        };
4021
4022        let dir = tempfile::tempdir().expect("tempdir");
4023        let path = dir.path().join("events.db");
4024        let stream = "seam-stream";
4025        let logs = Logs::new();
4026
4027        // Seeded under the older kind names, through the store itself: the
4028        // rows are ordinary appends, so they carry the version they were
4029        // written under.
4030        {
4031            let mut store = SqliteEventStore::open(&path, stream, &logs)
4032                .await
4033                .expect("open");
4034            store
4035                .append(obj(json!({
4036                    "kind": "legacy_opened",
4037                    "data": { "owner": "user-3", "scope_id": "scope-from-the-log" }
4038                })))
4039                .await
4040                .expect("the opening");
4041            store
4042                .append(obj(json!({
4043                    "kind": "budget_granted",
4044                    "data": { "amount": 100, "tag": "tokens" }
4045                })))
4046                .await
4047                .expect("the grant");
4048            store
4049                .append(obj(json!({
4050                    "kind": "legacy_response", "meta": { "beat": "b-1" },
4051                    "data": {
4052                        "content": [{ "type": "text", "text": "ok" }],
4053                        "usage": { "input_tokens": 7 }
4054                    }
4055                })))
4056                .await
4057                .expect("the response");
4058        }
4059
4060        // The seam the session reads through, carrying the test's own chain:
4061        // `resume_on` is the body of `resume` for exactly this, so the
4062        // session is otherwise the one production builds.
4063        let chain: Vec<Arc<dyn Upcaster>> = vec![Arc::new(RenameLegacyKinds)];
4064        let seamed = CurrentStore::new(
4065            Box::new(
4066                SqliteEventStore::open(&path, stream, &logs)
4067                    .await
4068                    .expect("reopen"),
4069            ),
4070            chain,
4071        );
4072        let mut resumed = Session::resume_on(None, seamed)
4073            .await
4074            .expect("resume through the seam");
4075
4076        // The restore read went through the chain: the opening was only a
4077        // `session_opened` after the step, and the scope came off it.
4078        assert_eq!(resumed.owner(), "user-3", "the owner the step revealed");
4079        assert_eq!(resumed.scope_id(), "scope-from-the-log");
4080        assert_eq!(
4081            resumed.grant().and_then(|g| g.tag.as_deref()),
4082            Some("tokens"),
4083            "and the grant with it"
4084        );
4085
4086        // …and so do `events`, the `tail` view and the balance fold.
4087        let log = resumed.events(0, usize::MAX).await.expect("events");
4088        assert_eq!(
4089            kinds(&log),
4090            [KIND_SESSION_OPENED, KIND_BUDGET_GRANTED, "llm_response"],
4091            "every read is projected"
4092        );
4093        let tail = resumed
4094            .view(VIEW_TAIL, Some(&obj(json!({ "n": 1 }))))
4095            .await
4096            .expect("tail");
4097        let last = &tail.as_array().expect("array")[0];
4098        assert_eq!(
4099            kind_of(last),
4100            "llm_response",
4101            "the view read the projected kind: {last}"
4102        );
4103        assert_eq!(last[FIELD_DATA]["usage"]["input_tokens"], json!(7));
4104        assert_eq!(
4105            remaining(&resumed).await,
4106            Some(100),
4107            "the balance folds too"
4108        );
4109
4110        // The stored rows were not rewritten: read them without the seam and
4111        // the old names are still there, under the version they were written
4112        // with.
4113        let raw = SqliteEventStore::open(&path, stream, &logs)
4114            .await
4115            .expect("reopen raw");
4116        let stored = raw.read(0, usize::MAX).await.expect("read raw");
4117        assert_eq!(kind_of(&stored[0]), "legacy_opened", "{}", stored[0]);
4118        assert_eq!(kind_of(&stored[2]), "legacy_response", "{}", stored[2]);
4119        // And a filtered read of the *stored* stream selects on those old
4120        // names, which is the obligation a renaming step takes on: the
4121        // projected name finds nothing until the rows are rewritten, and they
4122        // never are.
4123        assert!(
4124            raw.read_kinds(Some(&[KIND_SESSION_OPENED]), 0, usize::MAX)
4125                .await
4126                .expect("read raw")
4127                .is_empty(),
4128            "the kind filter selects on what is stored"
4129        );
4130        assert_eq!(
4131            stored[0].get(SCHEMA_VERSION_FIELD).and_then(Value::as_u64),
4132            Some(CURRENT_SCHEMA_VERSION),
4133            "an untouched row keeps the version it was written under: {}",
4134            stored[0]
4135        );
4136    }
4137
4138    /// (Upcasting seam) A stream whose ending is only visible *after* the
4139    /// step is still an ending: the disposable rule reads the projected log,
4140    /// not the stored one.
4141    #[tokio::test]
4142    async fn a_closed_stream_seen_through_the_seam_is_still_refused() {
4143        use crate::knl::{SqliteEventStore, Upcaster};
4144
4145        let dir = tempfile::tempdir().expect("tempdir");
4146        let path = dir.path().join("events.db");
4147        let stream = "seam-closed-stream";
4148        let logs = Logs::new();
4149
4150        {
4151            let mut store = SqliteEventStore::open(&path, stream, &logs)
4152                .await
4153                .expect("open");
4154            store
4155                .append(obj(json!({
4156                    "kind": "legacy_opened",
4157                    "data": { "owner": "user-3", "scope_id": "scope-from-the-log" }
4158                })))
4159                .await
4160                .expect("the opening");
4161            store
4162                .append(obj(json!({
4163                    "kind": "session_closed", "data": { "reason": "done" }
4164                })))
4165                .await
4166                .expect("the ending");
4167        }
4168
4169        let chain: Vec<Arc<dyn Upcaster>> = vec![Arc::new(RenameLegacyKinds)];
4170        let seamed = CurrentStore::new(
4171            Box::new(
4172                SqliteEventStore::open(&path, stream, &logs)
4173                    .await
4174                    .expect("reopen"),
4175            ),
4176            chain,
4177        );
4178        let err = Session::resume_on(None, seamed)
4179            .await
4180            .expect_err("a stream that ended must not be resumed");
4181        assert!(
4182            err.reason().contains("session is closed"),
4183            "{}",
4184            err.reason()
4185        );
4186    }
4187
4188    // -- the in-memory database, and reading the log with SQL ---------------
4189
4190    /// An in-memory session is a session, not a lesser one: it is a stream in
4191    /// a real database, so a second handle on its name finds the same log and
4192    /// resuming it restores the state.  What it cannot do is outlive the
4193    /// process — the database goes with the [`Logs`] that opened it — and
4194    /// nothing here pretends otherwise.
4195    ///
4196    /// Two ephemeral sessions of one host are two *streams* of one database,
4197    /// which is what lets one be opened from the other; they do not see each
4198    /// other's events, for the reason two streams never do.
4199    #[tokio::test]
4200    async fn an_in_memory_stream_is_resumable_while_it_is_open() {
4201        let logs = Logs::new();
4202        let mut s = Session::new(ANON.to_string(), Some(grant(100)), None, &logs)
4203            .await
4204            .expect("open");
4205        assert_eq!(s.reserve(30).await, Ok(true));
4206        s.append(obj(json!({ "kind": "note", "data": { "text": "hi" } })))
4207            .await
4208            .expect("append");
4209
4210        // The session id *is* the stream, so it is what a resume names.
4211        let store = SqliteEventStore::open_memory(s.id(), &logs)
4212            .await
4213            .expect("reopen the stream");
4214        let resumed = Session::resume(None, Box::new(store))
4215            .await
4216            .expect("resume");
4217        assert_eq!(resumed.owner(), ANON);
4218        assert_eq!(remaining(&resumed).await, Some(70), "the ledger came back");
4219        assert_eq!(
4220            kinds(&resumed.events(0, usize::MAX).await.expect("events")),
4221            vec![
4222                KIND_SESSION_OPENED,
4223                KIND_BUDGET_GRANTED,
4224                KIND_BUDGET_RESERVED,
4225                "note"
4226            ]
4227        );
4228
4229        // Another session of the same host is another stream of the same
4230        // database, and neither is the other's.
4231        let other = Session::new(ANON.to_string(), Some(grant(100)), None, &logs)
4232            .await
4233            .expect("open");
4234        assert_ne!(other.id(), s.id());
4235        assert_eq!(other.database(), s.database(), "one database, two streams");
4236        assert_eq!(
4237            other.len().await.expect("len"),
4238            2,
4239            "opened + granted, and no note"
4240        );
4241    }
4242
4243    /// A session reads its own log with SQL, and `$stream` is what makes
4244    /// "its own" true without the caller having to know its id.
4245    #[tokio::test]
4246    async fn a_session_reads_its_own_log_with_sql() {
4247        let mut s = new_session(None).await;
4248        s.append(obj(
4249            json!({ "kind": "msg_user", "data": { "content": "hi" } }),
4250        ))
4251        .await
4252        .expect("append");
4253        s.append(response(9)).await.expect("recorded");
4254
4255        let found = s
4256            .query(
4257                "SELECT kind, seq FROM events WHERE stream = $stream ORDER BY seq",
4258                QueryParams::None,
4259                &QueryOpts::default(),
4260            )
4261            .await
4262            .expect("query");
4263        assert!(!found.truncated);
4264        let kinds: Vec<&str> = found
4265            .rows
4266            .iter()
4267            .map(|row| row["kind"].as_str().expect("a kind"))
4268            .collect();
4269        assert_eq!(kinds, [KIND_SESSION_OPENED, "msg_user", "llm_response"]);
4270
4271        // A fold the kernel does not name — how many events of each kind —
4272        // is a query rather than a view it had to be taught.
4273        let counted = s
4274            .query(
4275                "SELECT kind, COUNT(*) AS n FROM events WHERE stream = $stream \
4276                 GROUP BY kind ORDER BY kind",
4277                QueryParams::None,
4278                &QueryOpts::default(),
4279            )
4280            .await
4281            .expect("query");
4282        assert_eq!(counted.rows.len(), 3);
4283
4284        // Another session's stream is not this one's, even in the same
4285        // process: the set a query reads is the set it named.
4286        let mut other = new_session(None).await;
4287        other
4288            .append(obj(json!({ "kind": "only_theirs" })))
4289            .await
4290            .expect("append");
4291        let mine = s
4292            .query(
4293                "SELECT kind FROM events WHERE stream IN $sessions",
4294                QueryParams::None,
4295                &QueryOpts::default(),
4296            )
4297            .await
4298            .expect("query");
4299        assert!(
4300            !mine.rows.iter().any(|row| row["kind"] == "only_theirs"),
4301            "{:?}",
4302            mine.rows
4303        );
4304
4305        // Reads keep working after the handle closed, like every other read.
4306        s.close(None).await.expect("close");
4307        assert!(s
4308            .query("SELECT 1 AS one", QueryParams::None, &QueryOpts::default())
4309            .await
4310            .is_ok());
4311    }
4312
4313    // -- children: the parent link, and the allocation that paid for it ------
4314
4315    /// A store for `stream` on the database `parent` is already on.
4316    ///
4317    /// What [`Session::open_child`] requires, built the way the bridge builds
4318    /// it: the parent is asked where it is, and the log that identity names is
4319    /// asked for ([`Logs::database`]) rather than opened again — which is what
4320    /// makes the child's stream a stream of the *same* log, and its half of
4321    /// the allocation part of the same transaction.
4322    async fn store_beside(parent: &Session, stream: &str) -> Box<dyn EventStore> {
4323        let db = parent.database().expect("the parent is on a database");
4324        let log = test_logs()
4325            .database(db)
4326            .await
4327            .expect("the parent's own log");
4328        Box::new(SqliteEventStore::on(log, stream))
4329    }
4330
4331    /// A fresh stream id, as the layer that opens a child mints one.
4332    fn stream_id() -> String {
4333        uuid::Uuid::new_v4().to_string()
4334    }
4335
4336    /// A second handle on `of`'s stream: another store on the same database,
4337    /// resumed.
4338    ///
4339    /// Two handles on one stream is a supported shape — the store serializes
4340    /// their writes — and it is the only way to have two callers allocating
4341    /// from one parent at the same time.
4342    async fn another_handle(of: &Session) -> Session {
4343        let db = of.database().expect("a database");
4344        let log = test_logs().database(db).await.expect("the session's log");
4345        let store = SqliteEventStore::on(log, of.id());
4346        let mut handle = Session::resume(None, Box::new(store))
4347            .await
4348            .expect("resume");
4349        handle.adopt_id(of.id().to_string());
4350        handle
4351    }
4352
4353    /// A session with `budget` on the file at `path`, so two handles can
4354    /// contend for one balance through two real connections.
4355    async fn file_session(path: &std::path::Path, budget: i64, logs: &Logs) -> Session {
4356        let stream = stream_id();
4357        let store = SqliteEventStore::open(path, stream.clone(), logs)
4358            .await
4359            .expect("open the stream");
4360        let mut session =
4361            Session::open_on(ANON.to_string(), Some(grant(budget)), None, Box::new(store))
4362                .await
4363                .expect("open");
4364        session.adopt_id(stream);
4365        session
4366    }
4367
4368    /// (2a) The allocation lands on both sides: the child opens with the
4369    /// units and with its parent named, and the parent's ledger carries the
4370    /// reservation that paid for them — one transaction, two streams.
4371    ///
4372    /// And nothing comes back.  A child closing is not a refund: the balance
4373    /// only rises when an owner grants.
4374    #[tokio::test]
4375    async fn an_allocation_opens_the_child_and_moves_the_units() {
4376        let mut parent = new_session(Some(100)).await;
4377        let stream = stream_id();
4378        let store = store_beside(&parent, &stream).await;
4379
4380        let mut child = parent
4381            .open_child(
4382                stream.clone(),
4383                "user-42".to_string(),
4384                Allocation::new(40),
4385                None,
4386                store,
4387            )
4388            .await
4389            .expect("the parent's balance covers it");
4390
4391        assert_eq!(child.id(), stream, "the child is the stream it was given");
4392        assert_eq!(child.owner(), "user-42");
4393        assert_eq!(remaining(&parent).await, Some(60), "the parent paid");
4394        assert_eq!(remaining(&child).await, Some(40), "and the child holds it");
4395
4396        // The child's log: it opened, and it opened with the grant. Both name
4397        // the parent, and the scope the handle reports is the one the opening
4398        // recorded — the child is a resumed session over what was written for
4399        // it, not a value built beside the log.
4400        let opened = child.events(0, usize::MAX).await.expect("events");
4401        assert_eq!(
4402            kinds(&opened),
4403            vec![KIND_SESSION_OPENED, KIND_BUDGET_GRANTED]
4404        );
4405        assert_eq!(
4406            field(&opened[0], FIELD_PARENT).as_str(),
4407            Some(parent.id()),
4408            "{}",
4409            opened[0]
4410        );
4411        assert_eq!(
4412            field(&opened[0], FIELD_SCOPE_ID).as_str(),
4413            Some(child.scope_id())
4414        );
4415        assert_eq!(*field(&opened[1], FIELD_AMOUNT), json!(40));
4416        assert_eq!(field(&opened[1], FIELD_PARENT).as_str(), Some(parent.id()));
4417        assert_eq!(
4418            field(&opened[1], FIELD_TAG).as_str(),
4419            Some("tokens"),
4420            "the child counts in the parent's unit unless it was renamed"
4421        );
4422
4423        // The parent's ledger: an ordinary reservation, naming where the
4424        // units went.
4425        let moves = ledger(&parent).await;
4426        assert_eq!(
4427            kinds(&moves),
4428            vec![KIND_BUDGET_GRANTED, KIND_BUDGET_RESERVED]
4429        );
4430        assert_eq!(*field(&moves[1], FIELD_AMOUNT), json!(40));
4431        assert_eq!(
4432            field(&moves[1], FIELD_CHILD).as_str(),
4433            Some(stream.as_str())
4434        );
4435
4436        // A child that closes gives nothing back.
4437        child.close(Some("done")).await.expect("close the child");
4438        assert_eq!(
4439            remaining(&parent).await,
4440            Some(60),
4441            "an allocation is a spend"
4442        );
4443    }
4444
4445    /// A child may count in a unit of its own, and the parent's own ledger
4446    /// entry stays in the parent's.
4447    #[tokio::test]
4448    async fn an_allocation_may_rename_the_unit_for_the_child() {
4449        let mut parent = new_session(Some(100)).await;
4450        let stream = stream_id();
4451        let store = store_beside(&parent, &stream).await;
4452        let child = parent
4453            .open_child(
4454                stream,
4455                "user-42".to_string(),
4456                Allocation {
4457                    amount: 10,
4458                    tag: Some("turns".to_string()),
4459                },
4460                None,
4461                store,
4462            )
4463            .await
4464            .expect("the allocation");
4465
4466        let opened = child.events(0, usize::MAX).await.expect("events");
4467        assert_eq!(field(&opened[1], FIELD_TAG).as_str(), Some("turns"));
4468        let moves = ledger(&parent).await;
4469        assert_eq!(
4470            field(&moves[1], FIELD_TAG).as_str(),
4471            Some("tokens"),
4472            "the parent's entry counts what the parent counts"
4473        );
4474    }
4475
4476    /// (2b) A balance that will not cover it: the refusal is recorded on the
4477    /// parent, nothing is opened, and the caller is told with the one class
4478    /// that reports a decision rather than a fault.
4479    #[tokio::test]
4480    async fn an_allocation_the_balance_cannot_cover_is_refused_and_recorded() {
4481        let mut parent = new_session(Some(10)).await;
4482        let stream = stream_id();
4483        let store = store_beside(&parent, &stream).await;
4484
4485        let err = parent
4486            .open_child(
4487                stream.clone(),
4488                "user-42".to_string(),
4489                Allocation::new(40),
4490                None,
4491                store,
4492            )
4493            .await
4494            .expect_err("10 does not cover 40");
4495        assert_eq!(err.kind(), KnlError::REFUSED, "{err}");
4496        assert!(
4497            !err.is_retryable(),
4498            "the same balance answers the same: {err}"
4499        );
4500        assert!(err.reason().contains("40"), "{err}");
4501
4502        // The balance did not move, and the refusal says what it was measured
4503        // against and which child it was for.
4504        assert_eq!(remaining(&parent).await, Some(10));
4505        let moves = ledger(&parent).await;
4506        assert_eq!(
4507            kinds(&moves),
4508            vec![KIND_BUDGET_GRANTED, KIND_BUDGET_REFUSED]
4509        );
4510        assert_eq!(*field(&moves[1], FIELD_AMOUNT), json!(40));
4511        assert_eq!(*field(&moves[1], FIELD_REMAINING), json!(10));
4512        assert_eq!(
4513            field(&moves[1], FIELD_CHILD).as_str(),
4514            Some(stream.as_str())
4515        );
4516
4517        // …and the child's stream was never written: a refused allocation
4518        // leaves no half-opened session behind.
4519        let unused = store_beside(&parent, &stream).await;
4520        assert_eq!(unused.len().await.expect("len"), 0);
4521    }
4522
4523    /// A parent with no budget has no ledger to measure against, so the
4524    /// allocation is allowed — the same rule `reserve` follows.  The child
4525    /// gets a ledger of its own all the same, because a grant is what starts
4526    /// one.
4527    #[tokio::test]
4528    async fn a_parent_with_no_budget_allocates_without_a_balance_to_measure() {
4529        let mut parent = new_session(None).await;
4530        let stream = stream_id();
4531        let store = store_beside(&parent, &stream).await;
4532        let child = parent
4533            .open_child(
4534                stream,
4535                "user-42".to_string(),
4536                Allocation::new(7),
4537                None,
4538                store,
4539            )
4540            .await
4541            .expect("there is no balance to refuse against");
4542
4543        assert_eq!(remaining(&parent).await, None, "still no budget here");
4544        assert_eq!(remaining(&child).await, Some(7));
4545    }
4546
4547    /// The tree is one log.  A child store on another database is refused
4548    /// before anything is written, because the two halves of an allocation
4549    /// share a transaction and a transaction covers one database.
4550    #[tokio::test]
4551    async fn a_child_on_another_database_is_refused() {
4552        let mut parent = new_session(Some(100)).await;
4553
4554        let stranger = stream_id();
4555        let elsewhere = SqliteEventStore::open_memory(stranger.clone(), &Logs::new())
4556            .await
4557            .expect("another in-memory database");
4558        let err = parent
4559            .open_child(
4560                stranger,
4561                "user-42".to_string(),
4562                Allocation::new(10),
4563                None,
4564                Box::new(elsewhere),
4565            )
4566            .await
4567            .expect_err("that is a different log");
4568        assert_eq!(err.kind(), KnlError::VALIDATION, "{err}");
4569        assert!(err.reason().contains("one log"), "{err}");
4570        assert_eq!(
4571            parent.len().await.expect("len"),
4572            2,
4573            "opened + granted, and nothing else"
4574        );
4575
4576        // A store that is not a database at all has no database to share, and
4577        // says so rather than being taken as "the same one".
4578        let mut single = Session::open_on(
4579            ANON.to_string(),
4580            Some(grant(50)),
4581            None,
4582            Box::new(MemEventStore::new()),
4583        )
4584        .await
4585        .expect("open");
4586        let err = single
4587            .open_child(
4588                stream_id(),
4589                "user-42".to_string(),
4590                Allocation::new(1),
4591                None,
4592                Box::new(MemEventStore::new()),
4593            )
4594            .await
4595            .expect_err("no database to open a child on");
4596        assert_eq!(err.kind(), KnlError::VALIDATION, "{err}");
4597    }
4598
4599    /// A child opens on a stream of its own.  A stream that already carries
4600    /// events is a bad argument rather than a decision about the balance:
4601    /// nothing is written on either side, so a stream id that came round twice
4602    /// cannot leave a log with two `session_opened`s and a `budget_granted`
4603    /// nobody's owner allowed.
4604    #[tokio::test]
4605    async fn a_child_does_not_open_on_a_stream_that_already_has_events() {
4606        let mut parent = new_session(Some(100)).await;
4607
4608        // A stream a child is already on: the same id, offered twice.
4609        let taken = stream_id();
4610        let store = store_beside(&parent, &taken).await;
4611        parent
4612            .open_child(
4613                taken.clone(),
4614                "user-42".to_string(),
4615                Allocation::new(10),
4616                None,
4617                store,
4618            )
4619            .await
4620            .expect("the first allocation");
4621        let before = parent.len().await.expect("len");
4622        assert_eq!(remaining(&parent).await, Some(90));
4623
4624        let store = store_beside(&parent, &taken).await;
4625        let err = parent
4626            .open_child(
4627                taken.clone(),
4628                "user-42".to_string(),
4629                Allocation::new(10),
4630                None,
4631                store,
4632            )
4633            .await
4634            .expect_err("that stream is a session already");
4635        assert_eq!(err.kind(), KnlError::VALIDATION, "{err}");
4636        assert!(err.reason().contains("already has events"), "{err}");
4637
4638        // Neither side moved: no reservation here, and over there the opening
4639        // and the grant the first child really got, with nothing after them.
4640        assert_eq!(parent.len().await.expect("len"), before);
4641        assert_eq!(remaining(&parent).await, Some(90));
4642        let occupied = store_beside(&parent, &taken).await;
4643        assert_eq!(occupied.len().await.expect("len"), 2);
4644
4645        // Any event is enough — the stream does not have to be a session for
4646        // a child's opening to be the wrong thing to write onto it.
4647        let stranger = stream_id();
4648        let mut seeded = store_beside(&parent, &stranger).await;
4649        seeded.append(response(1)).await.expect("seed the stream");
4650        let store = store_beside(&parent, &stranger).await;
4651        let err = parent
4652            .open_child(
4653                stranger,
4654                "user-42".to_string(),
4655                Allocation::new(10),
4656                None,
4657                store,
4658            )
4659            .await
4660            .expect_err("something is written there already");
4661        assert_eq!(err.kind(), KnlError::VALIDATION, "{err}");
4662        assert_eq!(parent.len().await.expect("len"), before);
4663        assert_eq!(seeded.len().await.expect("len"), 1, "and nothing was added");
4664
4665        // The ledger carries no refusal either: a balance that was never
4666        // measured has nothing to record.
4667        assert_eq!(
4668            kinds(&ledger(&parent).await),
4669            vec![KIND_BUDGET_GRANTED, KIND_BUDGET_RESERVED]
4670        );
4671
4672        // …and a stream nothing has been written to still opens.
4673        let fresh = stream_id();
4674        let store = store_beside(&parent, &fresh).await;
4675        let child = parent
4676            .open_child(
4677                fresh,
4678                "user-42".to_string(),
4679                Allocation::new(10),
4680                None,
4681                store,
4682            )
4683            .await
4684            .expect("an empty stream is what a child opens on");
4685        assert_eq!(remaining(&child).await, Some(10));
4686        assert_eq!(remaining(&parent).await, Some(80));
4687    }
4688
4689    /// A closed parent opens nothing — whether this handle knows it closed,
4690    /// or whether the ending is only in the log.  The second is decided
4691    /// *inside* the write, so a parent that closes between the read and the
4692    /// insert cannot get a child anyway.
4693    #[tokio::test]
4694    async fn a_closed_parent_opens_no_child() {
4695        let mut parent = new_session(Some(100)).await;
4696        // Taken while the stream is open: a resume refuses a closed one.
4697        let mut other = another_handle(&parent).await;
4698        parent.close(Some("done")).await.expect("close");
4699
4700        let stream = stream_id();
4701        let store = store_beside(&parent, &stream).await;
4702        let err = parent
4703            .open_child(
4704                stream,
4705                "user-42".to_string(),
4706                Allocation::new(1),
4707                None,
4708                store,
4709            )
4710            .await
4711            .expect_err("this handle closed");
4712        assert_eq!(err.kind(), KnlError::CLOSED, "{err}");
4713
4714        // The other handle never saw the ending; the decision does.
4715        let stream = stream_id();
4716        let store = store_beside(&other, &stream).await;
4717        let err = other
4718            .open_child(
4719                stream.clone(),
4720                "user-42".to_string(),
4721                Allocation::new(1),
4722                None,
4723                store,
4724            )
4725            .await
4726            .expect_err("the log carries an ending");
4727        assert_eq!(err.kind(), KnlError::CLOSED, "{err}");
4728        let unused = store_beside(&other, &stream).await;
4729        assert_eq!(unused.len().await.expect("len"), 0, "nothing was opened");
4730    }
4731
4732    /// A close records the children that had not ended, and lands anyway: the
4733    /// log never refuses a write, and "this ended while what it started was
4734    /// still going" is the fact worth having.
4735    #[tokio::test]
4736    async fn a_close_records_the_children_that_had_not_ended() {
4737        let mut parent = new_session(Some(100)).await;
4738
4739        let still_open = stream_id();
4740        let store = store_beside(&parent, &still_open).await;
4741        let _running = parent
4742            .open_child(
4743                still_open.clone(),
4744                "user-42".to_string(),
4745                Allocation::new(10),
4746                None,
4747                store,
4748            )
4749            .await
4750            .expect("the allocation");
4751
4752        let ended = stream_id();
4753        let store = store_beside(&parent, &ended).await;
4754        let mut done = parent
4755            .open_child(
4756                ended,
4757                "user-42".to_string(),
4758                Allocation::new(10),
4759                None,
4760                store,
4761            )
4762            .await
4763            .expect("the allocation");
4764        done.close(Some("done")).await.expect("close the child");
4765
4766        parent.close(Some("done")).await.expect("close");
4767        let boundary = parent
4768            .events(0, usize::MAX)
4769            .await
4770            .expect("events")
4771            .pop()
4772            .expect("the boundary");
4773        assert_eq!(boundary.kind(), KIND_SESSION_CLOSED);
4774        assert_eq!(
4775            *field(&boundary, FIELD_OPEN_CHILDREN),
4776            json!([still_open]),
4777            "the child that had ended is not among them: {boundary}"
4778        );
4779    }
4780
4781    /// A session with no children says nothing about them: an absent field,
4782    /// not an empty list, so "there were none" reads the same as it always
4783    /// did.
4784    #[tokio::test]
4785    async fn a_close_with_no_open_children_records_no_such_field() {
4786        let mut s = new_session(None).await;
4787        s.close(Some("done")).await.expect("close");
4788        let boundary = s
4789            .events(0, usize::MAX)
4790            .await
4791            .expect("events")
4792            .pop()
4793            .expect("the boundary");
4794        assert_eq!(
4795            data_field(&boundary, FIELD_OPEN_CHILDREN),
4796            None,
4797            "{boundary}"
4798        );
4799    }
4800
4801    /// Two callers allocating from one parent at the same time cannot both be
4802    /// paid: the decision and the write share a transaction, so the second
4803    /// measures a balance the first has already spent from.
4804    ///
4805    /// On a file, through two connections, because that is where the
4806    /// contention is real — the loser waits out the winner's `IMMEDIATE`
4807    /// transaction and then decides against what it committed.
4808    #[tokio::test]
4809    async fn two_children_allocating_at_once_never_over_allocate() {
4810        let dir = tempfile::tempdir().expect("tempdir");
4811        let path = dir.path().join("events.db");
4812        // The same collection every helper below reaches for, so both handles
4813        // and both child stores are on one log — which is where the decision
4814        // and the write share a transaction.
4815        let logs = test_logs();
4816
4817        let mut one = file_session(&path, 100, &logs).await;
4818        let mut two = another_handle(&one).await;
4819
4820        let first = stream_id();
4821        let second = stream_id();
4822        let first_store = store_beside(&one, &first).await;
4823        let second_store = store_beside(&two, &second).await;
4824
4825        let (a, b) = tokio::join!(
4826            one.open_child(
4827                first,
4828                "child-a".to_string(),
4829                Allocation::new(60),
4830                None,
4831                first_store
4832            ),
4833            two.open_child(
4834                second,
4835                "child-b".to_string(),
4836                Allocation::new(60),
4837                None,
4838                second_store
4839            ),
4840        );
4841
4842        // 60 + 60 is more than the parent had, so exactly one of them was
4843        // paid for and the sum of what was granted is within the balance.
4844        let granted: i64 = [&a, &b].iter().filter(|outcome| outcome.is_ok()).count() as i64 * 60;
4845        assert_eq!(granted, 60, "exactly one allocation may land");
4846        assert!(
4847            granted <= 100,
4848            "the sum of the grants is within the balance"
4849        );
4850
4851        let refused = match (&a, &b) {
4852            (Err(e), Ok(_)) | (Ok(_), Err(e)) => e,
4853            _ => panic!("one grant and one refusal, got {a:?} / {b:?}"),
4854        };
4855        assert_eq!(refused.kind(), KnlError::REFUSED, "{refused}");
4856
4857        // The parent's log tells the same story: it paid once and turned the
4858        // other down, and the balance is what is left after the one it paid.
4859        assert_eq!(remaining(&one).await, Some(40));
4860        assert_eq!(
4861            kinds(&ledger(&one).await),
4862            vec![
4863                KIND_BUDGET_GRANTED,
4864                KIND_BUDGET_RESERVED,
4865                KIND_BUDGET_REFUSED
4866            ]
4867        );
4868    }
4869
4870    /// An allocation is decided against the whole ledger and against the
4871    /// ending, so the kinds it asks the store for have to be exactly those.
4872    /// A kind added to the ledger and missed here would silently fall out of
4873    /// the balance an allocation measures.
4874    #[test]
4875    fn an_allocation_folds_the_ledger_and_looks_for_the_ending() {
4876        for kind in BUDGET_KINDS {
4877            assert!(
4878                ALLOCATION_KINDS.contains(kind),
4879                "the ledger's {kind} must reach an allocation's decision"
4880            );
4881        }
4882        assert!(ALLOCATION_KINDS.contains(&KIND_SESSION_CLOSED));
4883        assert_eq!(ALLOCATION_KINDS.len(), BUDGET_KINDS.len() + 1);
4884    }
4885}