CLAUDE.md@packages/session · git:20260927.62fa9b7 · 2026-09-27 · sha256 88d37520f47c332c
CLAUDE.md@packages/session git:20260927.62fa9b7A
Immutable. This exact content never changes and is served at /api/v1/blob/88d37520f47c332c.
# session
## Purpose
The session layer over one open storage handle: opening a session against a
chosen backend with migrate-on-open, boot-seeding a strand's registers,
typed access to the machine's register payloads through the machine codecs,
and the full context projection (pi §2.5) the runtime driver reads before
every generation. `session/repo` is the repository boundary above it —
administrative operations *over* sessions rather than inside one: forks
(pi §2.7), the precise rewrite (pi §2.9), and the text-erasure transform
the rewrite exists for. WP-C plus WP-C-full.
## Key Types
- `session/session.Session` — one open `Storage(Nil)` (handle type erased)
plus the lease-renewal capability the runtime's StorageWriter schedules,
`lease_interval_ms` (SQLite only; `None` for memory), and
`record_identity`, which projects the canonical session id into the
SQLite catalog row (a no-op for memory, which has no catalog).
`snapshot_reader` borrows bounded capture/page/fragment functions from the
same backend actor. It changes neither the frozen `Storage` record nor
storage ownership; reads after close fail like ordinary storage reads.
Each read takes a finite wait budget. ReadTimedOut ends only the wait,
not the storage work: the original gateway/session must fail without
retrying, and custody must retain the original store until drain is proved.
- `session/session.{id, parent_id, ensure_id, session_id_key,
parent_session_id_key}` — the canonical `core/ids.SessionId`
(`protocol-change/008`). `ensure_id` is boot bookkeeping in
`ensure_strand`'s shape: it mints one only if the reserved `session/id`
cell is empty, CAS-guarded on absence, so reopening a session always
yields the same id and a session that predates the concept gains one on
first open. `runtime/api.open` is the call site.
- `session/session.ensure_reserved_id` is the internal creation seam for a
catalogue reservation. Before runtime initialization, it establishes the
reserved canonical id with the same absence CAS as `ensure_id`. A retry
accepts only that id; an existing different id is never overwritten.
- `session/session.Cell(payload)` — a decoded register payload with the seq
it was read at. Every typed accessor returns one, because the seq is what
the caller's CAS expectation is built from.
- `session/session.{OpenError, SessionError}` — `SessionCorrupt(report)`
keeps a decode failure distinct from a store failure.
- `session/session.open_memory` / `open_sqlite` — the two constructors;
`open_sqlite` runs `migration_chain()`, the ordered migrate-on-open seam
every schema bump extends (empty today — storage version 1 is the only
version that has existed).
- `session/session.open_sqlite_owned` returns a session and a separate
close-and-retire capability. The custodian retains that capability until
every writer, reader and external effect has drained; a failed close
never becomes permission to retire the connection actor.
- `session/session.open_sqlite_custody` also returns a startup-link transfer
capability. The initializer publishes retirement before invoking transfer,
so its later death cannot kill the connection behind the custodian's handle.
Transfer returns the original actor PID for the instance host's fatal monitor.
- `session/session.ensure_strand` — idempotent boot seeding. It flattens
every commit refusal it does not already recognize into
`StoreFailure(BackendFault(..))`, `tx.LeaseLost` included: this layer
owns no tree to reopen, so the value it could carry has nowhere to go
and the reason string (`tx.describe_lease_loss`) is what a caller gets.
- `session/session.Projection` — an opaque projection policy built with
`projection()` then `with_projector(custom_type, ...)` and
`with_transform(...)`; `CustomView` is what a custom-entry projector
sees. `project` / `project_context` / `project_entries` / `project_scan`
run it, the last two purely over a branch scan the caller already has.
- `session/repo.{ForkScope, ForkDestination, ForkError, fork}` — copy one
coherent view of a source session (`ForkBranch` / `ForkTree`) into a
fresh destination (`ForkIntoMemory` / `ForkIntoSqlite`), as one atomic
destination transaction.
- `session/repo.{EntryRewrite, ValueRewrite, erase_text, erase_value,
rewrite_sqlite, rewrite_memory, MemoryRewrite, RewriteError}` — the
precise rewrite for both backends and the erase-a-string transforms it
exists for. Every rewrite takes both an entry transform and a value
transform (register payloads, usage details): the audit contract covers
every store a needle can reach, not just entries.
## Relationships
The `sqlight` module is supplied by the `sqlight_loom` Hex package, whose
`esqlite_loom` dependency carries the native query-retirement repair.
The module API is unchanged; the upstream packages must not coexist with
these forks because they define the same modules.
- **Depends on**: `core` (ids, entries, registers, tx), `storage` (both
backends and the handle), `machine` (register payload types and the total
codecs), `gleam_erlang` (the owned connection's PID type). Note that the
spec's DAG (§0.1) writes `C → A,B`; the machine
edge is real and load-bearing — typed register access cannot exist
without the payload codecs.
- **Depended on by**: `runtime` (the writer and the driver both hold a
`Session`), `client` (the gateway holds one), `conformance` (the
instrumented simulation store wraps one). `events` does not depend on
`session`, although the spec DAG writes `K → A,B,C`: its `gleam.toml`
names only `core`, `storage` and `telemetry` from the tree.
- **FFI**: none.
## Traffic
- **Actor messages**: none of its own. Every call is a synchronous
pass-through to the backend actor behind the `Storage` record.
The additional snapshot capability reaches that same actor without
creating a reader process or retaining a network-owned transaction.
- **Commits**: initialization and repository paths run outside the writer where no
supervision tree owns the store.
- `ensure_strand`'s three-write seed (`SetRegister` on `strand.leaf`,
`strand.config`, `strand.state`), committed **through the session
handle, bypassing the writer**, because it runs before the tree exists
(spec-gaps WP-E item 2). CAS-guarded with `Expect(..., seq: None)` on
all three; a losing concurrent seeder reads `StaleExpectation` as
success.
- `ensure_id`'s one-write mint (`SetRegister` on `fact.custom`
`session/id`), CAS-guarded with `Expect(..., seq: None)`; a losing
concurrent minter re-reads the winner's id rather than failing.
`ensure_reserved_id` shares this transaction but accepts the winner only
when it equals the reserved id. Both repair the SQLite identity projection
from the register rather than treating the projection as authority.
- `repo.fork`'s single destination transaction: `InsertEntry` per copied
entry (ids preserved, `seq`/`ts` re-stamped by the destination),
`SetRegister` for the destination strands' `strand.leaf` /
`strand.config` / `strand.state` and for the copied `fact.name` /
`fact.label`. `repo.rewrite_memory` rebuilds a session the same way but
retains *everything* — every register namespace verbatim plus
`InsertUsage` for each ledger row — because a rewrite erases content,
not history.
Every post-boot commit goes through `runtime/writer`.
- **Registers** — reads and decodes, one accessor per namespace:
`strand_configuration` (`strand.config`), `strand_state`
(`strand.state`), `strand_leaf` (`strand.leaf`), `last_result`
(`strand.last_result`), `op_meta` (`op.meta`), `op_state` (`op.state`),
`tool_arguments` (`op.tool_args`), `preparation` (`op.preparation`),
`pending_payloads` (`pending.entry`). `register_keys` lists a namespace's
keys for the machine's terminal cleanup. Writes the three seed registers
above; `repo.fork` additionally writes the destination's `strand.*`,
`fact.name`, and `fact.label`, and never copies `op.*`, `pending.entry`,
`strand.last_result`, or `fact.custom`.
- **Wire**: none in-process. `repo.rewrite_sqlite` reaches the file system
through `storage/sqlite.rewrite_into` (copy, rewrite, vacuum, rename).
## Invariants
- **Catalogue creation preserves its reserved identity.** The initializer
owns the fresh file and writer lease, then calls `ensure_reserved_id` before
runtime startup or effects. Retrying cannot mint a replacement identity or
overwrite an existing one.
- **One writer commits through a session.** Reads may come from anywhere —
both backends serialize through their own actor mailbox — but exactly one
StorageWriter process should hold the commit path.
- **Every register read is a total decode.** A payload that does not decode
is `SessionCorrupt(report)`, never a partial value and never a crash; the
caller faults rather than continuing on a guess.
- **Every typed read carries its seq.** The `Cell` seq is the input to the
caller's `SeqExpectation`; dropping it and re-reading later is how
lost-update bugs enter.
- **Strand seeding is idempotent and race-safe.** An existing
`strand.config` short-circuits; a concurrent seeder's `StaleExpectation`
is success, not an error.
- **The projection stops at the first compaction entry** and maps the
branch, oldest first, through pi §2.5's five rules in order: reverse to
oldest-first (a heading compaction opens the context with its summary and
retained tail); drop assistant responses whose stop reason is `error`,
`aborted`, or `deferred` while retaining genuine output-limit `length`;
run custom entries through the registered projectors; heal orphaned tool
calls; apply the `transform_context` hook.
- **An unregistered custom entry never enters context.** Projectors are
pure per-entry computation keyed by `custom_type`; registering a type
again replaces the earlier projector.
- **Healing happens at request construction, not at the fork** (pi §2.7,
spec-gaps WP-C-full item 5). A retained tool call whose result exists
nowhere later in the projected context gets a synthetic unknown-outcome
error result directly after its assistant message, before the transform
hook. On a settled history this is a no-op, which is what keeps
successive projections append-only.
- **`transform_context` is request-local and applied last.** It is never
persisted, and the harness trusts it to preserve conversation
well-formedness — a violating transform is an application defect, not a
storage validation case.
- **Compaction and branch-summary entries project as user messages** — pi
§2.5 leaves the provider mapping open, and this is the recorded
interpretation (spec-gaps WP-E item 7).
- **A fork is born identified, and the source is not touched.**
`repo.fork` mints the destination's own `SessionId` inside the copy
transaction and writes the source's id beside it as `session/parent` —
the only path in Loom that creates a session from a session, since
protocol `fork` makes a strand in the same session and the Agency's
children are strands too. A source with no id of its own forks to a
destination with no parent recorded: minting one into the source would
be the mutation fork forbids.
- **Fork never mutates the source, and the destination starts idle.** It
copies entries (ids preserved), strand configuration and leaves, and
semantic facts; it copies no operation, pending, or terminal-result
register and no usage row, so the destination's cost ledger starts at
zero. Forking into a destination that already holds entries is refused.
- **Fork and rewrite are defined over a quiescent source.** Reads serialize
through the storage actor, but successive reads interleaved with live
commits could observe two half-states, so the caller — an admin surface,
never the harness hot path — must ensure no writer is committing. The
SQLite rewrite enforces this by holding the writer lease for its whole
duration (a concurrent open is refused `LeaseHeld`); the in-process
operations trust their caller.
- **Erasure is total at the boundary.** `erase_text` rewrites string values
in an entry's canonical JSON and leaves object keys alone; the result is
decoded back through the entry codec, so a needle colliding with
structural vocabulary aborts the rewrite as corruption rather than
producing an unreadable store. `erase_value` does the same over the
free-form register and usage-details JSON, where there is no codec to
collide with (an id collision inside a register surfaces at the machine
codecs instead).
## Deep Docs
- [docs/architecture/orchestration.md](../../docs/architecture/orchestration.md)
— where the projection sits in the drive loop.
- [docs/architecture/durability.md](../../docs/architecture/durability.md) —
the tree, branches, and strands the projection walks.
- [docs/spec-gaps.md](../../docs/spec-gaps.md) — "From WP-E (`runtime`,
`session`)": boot seeding bypassing the writer, the projection mapping.
"From WP-C-full (`session`, `storage`)": branch-fork configuration
source, fork re-stamping placement, the absent parent-session record,
fact semantics across forks, healing placement, rewrite scope, and what
erasure leaves alone.
- [Root CLAUDE.md](../../CLAUDE.md) — repo ground rules and the doc graph.