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