# Session storage adapters

## Memory

`session_store_memory(max_sessions, max_bytes)` is process-local and has hard
entry and accounted-byte limits. It is appropriate for one-worker development,
tests, and deliberately non-durable applications. It is not shared by
Adaptive replacement workers.

## SQLite

Create a bounded native pool and pass it to `session_store_sqlite`:

```gray
set $database = sqlite_pool("var/sessions.sqlite", 4, 1000);
set $store = session_store_sqlite($database);
```

SQLite stores authenticated ciphertext, token digests, keyed user indexes,
and bounded expiry metadata. Rotation, grace consumption, user revocation,
and rate limiting use transactions. Multiple Gray processes may share the
same database.

Use a filesystem and SQLite deployment mode suitable for concurrent writers.
Back up the database and every still-required retiring key together.

## Accelerated SQLite

Use the experimental accelerator with an explicit durability product:

```gray
set $database = sqlite_pool("var/sessions.sqlite", 8, 1000);
set $store = session_store_sqlite_accelerated(
    $database,
    [
        "namespace": "accounts",
        "journal_path": "var/sessions.grayjournal",
        "durability": [
            "mode": "strict",
            "backend": "filesystem",
            "deadline_us": 2500,
            "flush_trigger_us": 500,
            "emergency_margin_us": 750
        ],
        "checkpoint_ms": 100,
        "maximum_sessions": 100000,
        "maximum_bytes": 268435456,
        "maximum_journal_bytes": 1073741824
    ]
);
```

The namespace is required and isolates applications sharing a database. A
same-user local broker owns the authoritative hot records and makes mutations
visible across Gray workers. In `strict` mode, the journal is durable before
the reply. SQLite readers may still lag until the checkpoint. Broker, policy,
capacity, journal, and durability failures fail closed. There is no automatic
strict-SQLite fallback.

The `filesystem` backend uses the platform's durable file flush primitive and
is currently the only backend accepted by the session runtime. `nvme_fua`
has a Linux doctor-only raw-device probe, but remains rejected by session
configuration because journal recovery and checkpoint integration are not
implemented yet. `dax` and `quorum` remain reserved and unavailable. None of
these names silently map to filesystem behavior. `session_stats()` reports
`durability_backend`.

Windowed mode must name a signed, promoted qualification:

```gray
"durability": [
    "mode": "windowed",
    "backend": "filesystem",
    "deadline_us": 2500,
    "flush_trigger_us": 500,
    "emergency_margin_us": 750,
    "qualification": [
        "certificate_path": "var/durability.json",
        "signature_path": "var/durability.sig.json",
        "public_key_path": "var/durability-public.pem"
    ]
]
```

The doctor is deliberately not allowed to authorize production by itself.
It creates signed probe evidence:

```text
gray doctor durability --backend filesystem --window 2.5ms --duration 30m \
  --journal var/sessions.grayjournal --runtime bin/session-benchmark-server \
  --evidence var/durability-probe.json \
  --signature var/durability-probe.sig.json \
  --key durability-private.pem
```

`gray.durability-probe.v3` has `authorization: "probe-only"`. Ordinary
windowed startup rejects it. The dedicated qualifier must next run the exact
server binary through three independent 100 request/second update runs and
three rotation runs. Every run lasts three minutes after warm-up, bootstraps
100,000 cookie-bearing sessions, uses 256 persistent clients, includes the
broker in resource accounting, and requires zero margin or durability
violations.

After those six runs, `promote_durability_probe.py` independently verifies
their results and per-file inventory, writes a burn-in attestation, and signs
`gray.durability-certificate.v3`. The promoted certificate binds the probe,
runtime, application burn-in, host, journal filesystem, deadline, trigger,
margin, and signer. Only this `runtime-windowed` certificate is accepted by
ordinary startup. The dedicated launcher enforces probe, burn-in, promotion,
and performance-matrix ordering.

Linux developers with an expendable, unmounted NVMe namespace can exercise
the native FUA primitive separately:

```text
gray doctor durability --backend nvme_fua --window 2.5ms --duration 30m \
  --device /dev/nvme1n1 \
  --confirm-destructive-device EXACT-SYSFS-SERIAL \
  --runtime bin/session-benchmark-server \
  --evidence var/nvme-fua-probe.json \
  --signature var/nvme-fua-probe.sig.json \
  --key durability-private.pem
```

This command destroys data on the confirmed namespace. Gray requires a block
device with no partitions or holders, opens it with `O_EXCL`, takes a
nonblocking exclusive lock, revalidates the opened device identity, and
requires the exact `/sys` serial. It writes an ownership header, then submits
real NVMe Write commands through `NVME_IOCTL_SUBMIT_IO` with the FUA control
bit. It does not use `fdatasync` or a mapped file.

The signed `gray.durability-probe.v4` result remains
`authorization: "probe-only"` and `runtime_authorized: false`, even if the
observed run passes. It explicitly records that whole-host survival is
unverified and device-loss survival is false. A temporary loop-backed NVMe
target can verify the ioctl/schema/signature path, but it is not hardware
latency or power-loss evidence.

The probe uses the same mapped-write, `msync`, and durable-flush shape as the
runtime journal over two physically preallocated 16 MiB segments. It
repeatedly crosses the segment boundary while a competing durability stream
and CPU load are active. A pwrite-only microbenchmark is not accepted.

Both probe and promoted certificate say
`assurance_model: "empirical-fail-closed"`. This is an observed eligibility
decision, not a hard promise about every future filesystem flush. The first
unforeseen outlier can exceed the target before Gray can revoke the
certificate. Applications requiring acknowledgement only after durability
must use `strict-after-durable`; a hard low-latency acknowledgement contract
still requires runtime integration of qualified FUA-capable PLP storage or a
synchronous replication quorum.

Gray's accelerated-session test contract includes a bounded real-time
history checker. It records invocation/completion intervals and observable
results from lookup, generation-conditional update, rotation, and revocation
operations issued concurrently through two workers. A backtracking
linearizability check must find a legal serial history that also preserves
every completed-before-invoked ordering edge. Hand-written race assertions
remain useful, but do not replace this history gate.

Recovery tests also terminate the broker exactly after the mapped header,
ciphertext, authentication tag, commit-marker publication, durable flush,
and SQLite checkpoint commit. A zero commit marker is the sole discardable
final torn-record state; a nonzero invalid marker or authenticated-record
failure still fails startup closed.

Checkpoint reclamation advances an incremental boundary frontier. Each
journal boundary is examined once until a physical prefix replacement resets
the frontier; checkpoints do not repeatedly rescan the entire retained
deque. `journal_reclaim_boundary_scans` is cumulative process-local scan
work, while `journal_reclaim_scan_frontier_entries` is the number of current
boundaries already classified as reclaimable.

These hooks model abrupt process death (`_Exit`) and are not host power-loss
evidence. Power-loss qualification requires a dedicated runner capable of
cutting or faulting the storage boundary independently of the Gray process.

Use `session_stats($store)` for bounded connection, hot-state, journal,
durability, checkpoint, restart, and cache counters. These metrics never
contain cookies, digests, identifiers, or stored values. The accelerator is
not yet qualified for production or for its 50,000/s sub-millisecond release
gate. Windows support and dedicated macOS/Windows/Linux qualification remain
open.

The configured deadline is an empirically qualified measured SLO only in
`windowed-before-flush` mode. Strict mode waits for the durable flush and
reports the device latency without converting a slow successful flush into a
deadline failure. Pre-journal admission backpressures newer windowed work
when the oldest outstanding acknowledgement approaches that window. A late
successful flush increments `durability_slo_violations`, records its sequence,
revokes the certificate, and changes admission to `Failed` without pretending
an earlier response can be withdrawn. Qualification requires zero measured
violations.

Admission prediction is seeded from the signed qualification maximum even
before the first runtime mutation; it is not initialized optimistically at
zero. A real journal flush that remains inside the absolute deadline but
exhausts the configured emergency margin first records `Backpressured`, then
revokes certification and enters `Failed`. The runtime never learns that
unsafe tail as a new relaxed baseline. `durability_runtime_margin_events`,
`durability_qualification_maximum_flush_us`, and
`durability_qualification_p999_flush_us` expose these bounded decisions.
`durability_qualification_assurance_model`,
`durability_qualification_burn_in_profile`, and
`durability_qualification_burn_in_evidence_sha256` make the authorization
boundary machine-readable.

`active_record_bytes` counts authoritative session records,
`retired_alias_bytes` counts rotation-grace aliases (including exhausted
negative tombstones), and `hot_bytes` is their bounded sum. Expired aliases
are reclaimed after their grace deadline. Capacity admission happens before
the journal sequence advances; it never evicts an arbitrary live alias.
One chain may contain no more than sixteen simultaneously retained aliases.
The seventeenth is rejected before journal admission. Expiration and pruning
start a new depth-one epoch, so applications are not limited to sixteen
rotations over a session's lifetime. Hot recovery, authenticated raw-journal
metadata, broker traversal, and SQLite recovery enforce the same bound and
reject cycles, forks, dangling links, self-maps, grace-counter inconsistencies,
and active/retired digest ambiguity.
Mutation operation IDs remain hot only until their SQLite checkpoint.
Transport retries explicitly carry the same ID and consult the durable table
only when the hot entry has already checkpointed. Expired durable IDs are
removed through the indexed retry-window cleanup, so normal mutations do not
perform a deduplication query and neither tier grows for the life of the
process. `mutation_identity_hot_entries` and
`mutation_identity_checkpoint_hits` expose this behavior without identifiers.
Hot deduplication entries keep their 16-byte operation IDs and 32-byte
request hashes inline rather than in heap-owning Strings.
`mutation_identity_hot_key_bytes` and
`mutation_identity_hot_request_hash_bytes` expose those fixed payload sizes;
they never expose the bytes themselves.
Session records and their shared-ownership control blocks are allocated
together from a store-owned synchronized slab. The store outlives every
session handle, including handles to expired or rotated records.
`record_slab_live_allocations`, `record_slab_live_bytes`,
`record_slab_reserved_bytes`, and `record_slab_upstream_allocations` make
pool reuse and bounded retained capacity observable. Broker-backed workers
prefix their local-cache allocator counters with `worker_`; the unprefixed
counters describe the authoritative broker.
AES-GCM operations similarly reuse one explicitly reset OpenSSL cipher
context per active runtime thread. `cipher_context_creations` and
`cipher_context_reuses` expose that bounded churn; broker workers publish
their process-local values with the `worker_` prefix. Context reset occurs
before every encrypt or decrypt, including after authentication failure.
Strict SQLite, Redis, and accelerator-checkpoint record encryption also
uses one cleansed serialization buffer per active runtime thread. Gray
retains at most 64 KiB of capacity in each such buffer and releases larger
allocations after use. Journal and IPC records that must outlive the call
remain independently owned. `serialization_buffer_creations`,
`serialization_buffer_reuses`,
`serialization_buffer_oversize_discards`, and
`serialization_buffer_retained_limit_bytes` expose this policy. Broker
workers use the corresponding `worker_`-prefixed fields.
The accelerator checkpoint connection also retains its session-generation
validation and encrypted-session upsert statements. Both are reset and have
all bindings cleared after each checkpointed record.
`sqlite_save_statement_cache_hits` and
`sqlite_save_statement_cache_misses` distinguish that reusable checkpoint
path from strict SQLite's one-transaction statement lifecycle.

## Redis and Valkey

```gray
set $client = redis_pool("127.0.0.1", 6379, 16, 1000);
set $store = session_store_redis($client, "accounts:");
```

The prefix is a strict application namespace. Gray adds a deterministic hash
tag so each Lua operation stays within one Redis Cluster slot. Records and
retired aliases have bounded TTLs. Save, per-user session limits, rotation,
grace consumption, account rate limits, and log-out-everywhere are atomic Lua
operations.

The current adapter uses bounded synchronous Redis socket calls. TLS, ACL
authentication configuration, Sentinel discovery, topology refresh, and
qualification against a real Redis/Valkey daemon remain explicit deployment
boundaries. Repository tests use a controlled loopback RESP fixture and do not
claim full server compatibility.

## Shared rules

- Durable startup fails when `GRAY_SESSION_KEYS` is absent or malformed.
- An unknown key ID, authentication-tag failure, timeout, or protocol failure
  fails the request; there is no memory fallback.
- Raw tokens and plaintext values are never written to a durable store.
- Values are limited to 16 MiB, one million collection elements, and 64
  nesting levels; configured store limits may be smaller.
- Handles, cycles, non-finite numbers, invalid Strings, and incompatible typed
  schemas are rejected.
- `Bytes` preserves NUL and arbitrary non-UTF-8 octets exactly.
