# loregd

> Local Registry Daemon — the SQLite-backed RSI source that provides the Machine and Users hives: storage, concurrency, and request handling.

---

# 1.1 Overview

_Peios / Advanced Peios / loregd / Introduction_

> loregd is the local registry source — it holds hives in SQLite and serves them to the kernel over RSI. Where it sits, and what this manual covers.

**loregd** — the Local Registry Daemon — is the primary registry source
for Peios. It holds one or more registry hives in SQLite databases and
serves them to the kernel's registry subsystem over the Registry Source
Interface (RSI).

loregd is not architecturally special. Any process that implements the
RSI contract can serve as a registry source, and the kernel is
source-agnostic: it does not know or care that the process answering for
a hive keeps its data in SQLite. What makes loregd special is
operational. It is the source that provides the `Machine\` and `Users\`
hives at boot, which puts it on the critical path to a running system —
if loregd does not come up, very little else does.

## 1.1.1 Where loregd sits

The registry is split across a trust boundary. The kernel side owns the
namespace, the layer model, access checks, watches, and transactions; it
holds no storage of its own. The source side owns bytes on disk and
answers questions about them. loregd is a source, and everything in this
manual describes the lower half of that split.

Three consequences run through the whole design:

- **loregd stores; it does not decide.** It never resolves layers, never
  filters results by visibility, and never applies a security descriptor.
  It returns every layer entry it holds and lets the kernel work out
  which one wins. The security descriptors in its tables are opaque
  payload to it.
- **GUIDs come from the kernel.** loregd does not mint key identities. It
  records the GUID it is given, and uses it as the primary key.
- **Sequence numbers come from the kernel.** loregd stores them and
  reports the maximum it holds at registration, but never allocates one.

## 1.1.2 What this manual covers

The command line and startup sequence; the SQLite schema backing a hive
and the in-memory store backing volatile keys; the connection model,
write serialisation, and how requests are dispatched; and the handling of
each RSI operation, including transactions and enumeration ordering.

The kernel half of the registry — the namespace, layers, watches, access
control — is [chapter 5 of the Peios Kernel TRM](/peios/advanced-peios/peios-kernel/lcs/overview.md),
and the RSI wire protocol itself is
[specified in PSPK](/peios/advanced-peios/pspk/registry-source-interface/scope.md). Key schemas
belong to the subsystems that own them, and loregd's supervision as a
service belongs to the init system's manual.

---

# 1.2 Terminology

_Peios / Advanced Peios / loregd / Introduction_

> The registry vocabulary this manual borrows from the kernel-side documentation, and the terms loregd adds.

The registry's own vocabulary — hive, key, value, layer, source, watch,
security descriptor, sequence number — is defined by the kernel-side
registry documentation and is used here unchanged.

The terms below are specific to loregd.

**Hive database.** The SQLite database file backing one hive. Each hive
registered by loregd has its own file, whose path is given on the command
line (§2.1). Referred to in SQL as the `main` schema.

**Volatile database.** The in-memory SQLite database backing one hive's
volatile keys, attached to that hive's connections under the schema name
`volatile` (§3.3). It mirrors the hive database's tables, holds no data at
startup, and is destroyed with the process.

**Folded name.** The case-folded form of a key name, value name, or child
name, stored in a `_folded` column beside the canonical case-preserving
name and used for all case-insensitive comparison (§3.4).

**Write connection.** The single SQLite connection per hive through which
every mutation passes. Its uniqueness is what serialises writes (§4.1).

**Read pool.** The fixed set of connections serving reads that are not
part of a transaction, selected round-robin (§4.1).

**Snapshot connection.** A dedicated connection opened for one read-only
transaction, pinning a point-in-time view of the hive database for that
transaction's lifetime (§4.3).

**Orphan.** A key record that no path entry in any layer points at. Orphans
are cleaned up at startup (§2.2) and are reported by `RSI_DELETE_LAYER` as
the keys its deletion left unreachable (§5.6).

---

# 2.1 Command Line

_Peios / Advanced Peios / loregd / Startup_

> loregd is configured entirely by its argument vector — the hive declarations it takes, and how they are validated.

loregd is configured entirely by its argument vector. It takes one or
more hive declarations, each naming a hive and the SQLite database file
that backs it:

```
loregd <HiveName>=<DatabasePath> [<HiveName>=<DatabasePath> ...]
```

For example:

```
loregd Machine=/var/state/registry/machine.regdb Users=/var/state/registry/users.regdb
loregd Machine=/var/state/registry/machine.regdb Users=/var/state/registry/users.regdb Roles=/var/state/registry/roles.regdb
```

Each argument is split at its **first** `=`, so a database path may
itself contain `=`. Every declared hive is registered with the kernel at
startup (§2.2).

## 2.1.1 Argument validation

loregd rejects the invocation and exits with a non-zero status if any of
the following hold:

| Condition | Reason |
|---|---|
| No hive arguments at all | At least one hive is required. |
| An argument with no `=` | Not a hive declaration. |
| An empty hive name or empty path | Neither is meaningful. |
| A relative database path | Paths are required to be absolute. |
| A hive name containing `\`, `/`, or NUL | These are path separators and terminators in the registry namespace. |
| The hive name `CurrentUser`, in any case | Reserved by the kernel as a per-token alias; no source may claim it. |
| Two declarations of the same hive name | Duplicates are detected on the **folded** name, so `Machine` and `MACHINE` collide. |

Hive-name comparison is case-insensitive throughout — for duplicate
detection here, and for routing requests later — but the case as written
on the command line is preserved and is what loregd presents to the
kernel when it registers.

## 2.1.2 Configuration

loregd has no configuration file and reads no configuration from the
registry. This is deliberate: loregd *is* the configuration store, and a
store that had to read its own configuration in order to start could not
start.

Everything about how it behaves comes from three places: the command-line
arguments above, the contents of the SQLite databases they name, and
compiled-in constants (§4.1).

`NOTIFY_SOCKET` is the one environment variable loregd consults, and it
carries no behavioural setting. When it is set, loregd treats it as a
service-manager readiness socket: once the hives are registered and the
request loop is about to begin, it connects and sends `READY=1`. When it
is unset, the step is skipped.

loregd also opens `/dev/console` for writing at startup and, if that
succeeds, redirects its own diagnostic log there. This is what makes
loregd's failures visible during early boot, before any log daemon
exists.

---

# 2.2 Startup Sequence

_Peios / Advanced Peios / loregd / Startup_

> Startup runs to completion before a single request is accepted, and any failure in it is fatal.

Startup runs to completion before loregd accepts a single request. Any
failure in it is fatal: loregd logs the error and exits non-zero rather
than serving a hive it could not fully prepare.

## 2.2.1 1. Parse and validate arguments

Extract the hive name to database path mapping and apply the validation
in §2.1.

## 2.2.2 2. Open each hive database

For each declared hive, create the database file's parent directory if it
is absent (mode `0755`), then open — or create — the SQLite database at
that path. loregd owns its storage location, so a first boot onto an
empty `/var/state` is expected to work without anything having prepared
the directory.

Four pieces of connection state are established immediately:

- `PRAGMA journal_mode=wal`. The result is read back and checked; if the
  database does not report `wal`, the open fails. WAL mode is what allows
  concurrent readers alongside a writer (§4.1), so silently running
  without it is not acceptable.
- `PRAGMA foreign_keys=ON`. Neither schema declares a foreign key, so
  this constrains nothing.
- `PRAGMA busy_timeout=25000` — 25 seconds (§4.3).
- The volatile database is attached (§3.3).

Each `database/sql` handle is limited to a single underlying connection.

## 2.2.3 3. Attach the volatile store

Each hive gets an in-memory SQLite database, attached to its connections
under the schema name `volatile` (§3.3). The attach happens as part of
opening the connection in step 2; the volatile *tables* are created in
step 4.

## 2.2.4 4. Establish the schema

If the database has no `schema_version` table, it is new: loregd creates
the persistent tables, creates the volatile tables, and stamps the schema
version, in one transaction.

If the version is present, it is compared against the version loregd
supports. A newer version aborts startup, and so does an older one
(§3.1).

## 2.2.5 5. First-boot root key

For each hive, look for a key with no parent. If none exists, this is the
hive's first boot: loregd generates a random 16-byte GUID for the root
key, builds the default hive-root security descriptor, and inserts the
root key record.

The default root descriptor grants SYSTEM and Administrators full access
to the key, grants Authenticated Users read access, marks all three as
container-inheritable, and sets both owner and group to SYSTEM.

## 2.2.6 6. Crash recovery

SQLite's own WAL recovery handles any transaction that was uncommitted
when the process died; it happens when the database is opened and needs
nothing from loregd.

What loregd does on top of that is clean up **orphaned keys** — key
records that no path entry in any layer points at. These can survive a
crash between a key's creation and the creation of the path entry naming
it. In one transaction, loregd deletes the values belonging to orphaned
keys, then their blanket tombstones, then the key records themselves.

The hive root is exempt: it legitimately has no parent and no path entry
pointing at it, so orphan detection skips keys whose `parent_guid` is
null.

## 2.2.7 7. Compute the maximum sequence number

For each hive, take the maximum `sequence` across the path entries,
values, and blanket tombstones — in both the persistent and the volatile
tables — then take the maximum across all hives. That single global
figure is what loregd reports at registration, so the kernel can resume
allocating sequence numbers above everything already stored.

In practice the volatile tables are empty at this point in startup and
contribute nothing.

## 2.2.8 8. Open the registry device

Open `/dev/pkm_registry`. The kernel requires `SeTcbPrivilege` in the
calling thread's token to permit this; loregd performs no check of its
own and relies on the kernel to refuse.

## 2.2.9 9. Register the hives

Issue `REG_SRC_REGISTER` with every hive name, its root key GUID, and the
global maximum sequence number from step 7. The registration flags are
zero: loregd registers global hives only and never private ones.

## 2.2.10 10. Signal readiness and serve

Send readiness to `NOTIFY_SOCKET` if it is set (§2.1), install the
termination signal handler (§2.3), and enter the request loop (§4.2).

---

# 2.3 Exit and Shutdown

_Peios / Advanced Peios / loregd / Startup_

> What ends the loregd process, what pointedly does not, and what happens to the data in each case.

## 2.3.1 What ends the process

loregd exits when any of the following happens:

- **The kernel closes the registry device.** Reading from
  `/dev/pkm_registry` returns end-of-file, the request loop returns, and
  loregd shuts down cleanly with status 0. This is the normal path when
  the registry subsystem goes away.
- **A termination signal arrives.** `SIGTERM` and `SIGINT` are trapped.
  The handler closes the device, which unblocks the read loop and
  produces the same clean shutdown as above. This is how the service
  manager stops loregd.
- **A request cannot be framed.** If a message read from the device
  cannot be parsed as an RSI request, loregd treats it as unrecoverable
  and exits non-zero.
- **Startup fails.** Any error in §2.2 is fatal.

On shutdown, in-flight requests are drained before the process exits
(§4.2), and every hive's read connections and write connection are
closed.

## 2.3.2 What does not end the process

A storage failure during request handling does **not** terminate loregd.
Errors from SQLite while serving a request — including I/O errors — are
converted into an `RSI_STORAGE_ERROR` response and the daemon carries on
serving. There is no corruption detector and no disk-full detector that
takes the process down; a database that has become unreadable will
produce a stream of storage errors rather than an exit.

> [!NOTE]
> This is worth knowing when diagnosing a system whose registry has
> started failing: the symptom is failing operations, not a dead daemon,
> and loregd will still be running and still registered.

## 2.3.3 What happens to the data

Persistent data is durable at the point each transaction commits;
SQLite finalises any outstanding WAL state as the connections close.

Volatile data does not survive. The in-memory databases holding volatile
keys are destroyed with the process (§3.3), which is the entire point of
volatility. When the kernel observes the source disconnect, it marks
every hive loregd served as unavailable.

---

# 3.1 Schema Version

_Peios / Advanced Peios / loregd / Storage_

> The single-row table every hive database carries, the current version, and how a version mismatch is handled.

Every hive database carries a schema version in a single-row table:

```sql
CREATE TABLE schema_version (
    version INTEGER NOT NULL
);
```

The current version is **1** — the only version that has existed.

loregd checks the version as it opens each database (§2.2, step 4) and
takes one of three paths:

| State | Behaviour |
|---|---|
| No `schema_version` table | The database is new. loregd creates the persistent tables, the volatile tables, and inserts version 1, all in one transaction. |
| Version equals 1 | Normal startup. |
| Version greater than 1 | Startup fails. The database was written by a newer loregd, and proceeding risks corrupting it by writing through an older understanding of its layout. |
| Version less than 1 | Startup fails. |

**There are no migrations.** loregd carries no migration table, no
migration step list, and no upgrade path; an older database is reported
as requiring migration and startup stops there. Since 1 is the only
version ever assigned, this does not arise in practice — but a second
version cannot be stamped without building the migration machinery
first.

Two details of the check are worth knowing. The `schema_version` table
has no primary key, uniqueness constraint, or check constraint, so a
second row is not detected: the first row read wins. And because every
table is created with `IF NOT EXISTS`, a database holding the data tables
but no `schema_version` table is stamped as version 1 without any
validation that its contents match that layout.

---

# 3.2 Persistent Tables

_Peios / Advanced Peios / loregd / Storage_

> The four data tables every hive database has — keys, path entries, values and blanket tombstones — and their columns.

Each hive is one SQLite database, and every hive database has the same
four data tables. loregd creates them on first boot (§2.2, step 4).

The tables hold what the kernel gives loregd and nothing derived from it.
Security descriptors are stored as opaque blobs, GUIDs and sequence
numbers are assigned by the kernel, and no table records a resolved or
filtered view of anything.

## 3.2.1 keys

```sql
CREATE TABLE keys (
    guid           BLOB NOT NULL PRIMARY KEY,
    name           TEXT NOT NULL,
    name_folded    TEXT NOT NULL,
    parent_guid    BLOB,
    sd             BLOB NOT NULL,
    volatile       INTEGER NOT NULL DEFAULT 0,
    symlink        INTEGER NOT NULL DEFAULT 0,
    last_write_time INTEGER NOT NULL
);
```

| Column | Meaning |
|---|---|
| `guid` | The 16-byte key GUID assigned by the kernel. Primary key. |
| `name` | The key's own name component, with case preserved as written. |
| `name_folded` | The folded form of `name` (§3.4), used for case-insensitive lookup. |
| `parent_guid` | The parent key's GUID; null for the hive root, which is how the root is identified. |
| `sd` | The security descriptor, in binary self-relative form. Opaque to loregd. |
| `volatile` | 1 for a volatile key, 0 for a persistent one. In this table it is always 0 — volatile keys live in the volatile database (§3.3). |
| `symlink` | 1 if the key is a symbolic link. |
| `last_write_time` | Unix nanoseconds. |

## 3.2.2 path_entries

```sql
CREATE TABLE path_entries (
    parent_guid       BLOB NOT NULL,
    child_name        TEXT NOT NULL,
    child_name_folded TEXT NOT NULL,
    layer             TEXT NOT NULL,
    target_type       INTEGER NOT NULL,
    target_guid       BLOB,
    sequence          INTEGER NOT NULL,
    PRIMARY KEY (parent_guid, child_name_folded, layer)
);

CREATE INDEX idx_path_entries_target
    ON path_entries (target_guid)
    WHERE target_type = 0;
```

A path entry is one layer's opinion about one child name under one
parent. Several layers may hold entries for the same name; resolving
between them is the kernel's job, not loregd's.

| Column | Meaning |
|---|---|
| `parent_guid` | The parent key's GUID. |
| `child_name` | The child name with case preserved. |
| `child_name_folded` | The folded form, which is what the primary key uses — so a name collides case-insensitively within a layer. |
| `layer` | The layer name. Compared as binary, so layer names *are* case-sensitive, unlike key names. |
| `target_type` | 0 for a GUID entry (the key exists in this layer), 1 for HIDDEN (a tombstone masking lower layers). |
| `target_guid` | The target key's GUID when `target_type` is 0; null for HIDDEN. |
| `sequence` | The kernel-assigned sequence number. |

The partial index on `target_guid` covers only non-HIDDEN rows. It is
what makes the reverse lookup — which path entries point at this key —
cheap, and that reverse lookup is what orphan detection (§2.2, step 6)
and `RSI_DROP_KEY` need.

## 3.2.3 values

```sql
CREATE TABLE [values] (
    key_guid       BLOB NOT NULL,
    name           TEXT NOT NULL,
    name_folded    TEXT NOT NULL,
    layer          TEXT NOT NULL,
    type           INTEGER NOT NULL,
    data           BLOB,
    sequence       INTEGER NOT NULL,
    PRIMARY KEY (key_guid, name_folded, layer)
);
```

| Column | Meaning |
|---|---|
| `key_guid` | The key this value belongs to. |
| `name` | The value name, case preserved. The empty string is the key's default value. |
| `name_folded` | The folded form; also the empty string for the default value. |
| `layer` | The layer this value entry belongs to. |
| `type` | The registry value type — `REG_SZ` is 1, `REG_DWORD` is 4, and so on. `REG_TOMBSTONE` (`0xFFFF`) marks a per-value tombstone. |
| `data` | The value payload; null for a tombstone. |
| `sequence` | The kernel-assigned sequence number. |

`values` is a reserved word in SQL, so every reference to this table is
quoted — `[values]`, or `main.[values]` and `volatile.[values]` when the
schema is named explicitly. Unquoted, it is a syntax error.

## 3.2.4 blanket_tombstones

```sql
CREATE TABLE blanket_tombstones (
    key_guid       BLOB NOT NULL,
    layer          TEXT NOT NULL,
    sequence       INTEGER NOT NULL,
    PRIMARY KEY (key_guid, layer)
);
```

A blanket tombstone hides *every* value a key holds in the layers beneath
it, rather than naming one value the way a `REG_TOMBSTONE` entry does.
One row per key per layer.

---

# 3.3 The Volatile Store

_Peios / Advanced Peios / loregd / Storage_

> Volatile keys never reach the hive file, so each hive attaches a second in-memory database mirroring the persistent schema.

Volatile keys exist only for the lifetime of the running system, so they
never reach the hive's database file. Each hive instead gets a **second
SQLite database, held entirely in memory**, attached to its connections
under the schema name `volatile`:

```
file:<HiveName>_volatile?mode=memory&cache=shared
```

The hive name in the URI is the case-preserved name from the command
line, which is what keeps one hive's volatile database distinct from
another's.

## 3.3.1 A mirror of the persistent schema

The volatile database carries the same four tables as the persistent one
— `keys`, `path_entries`, `[values]`, and `blanket_tombstones` — with
identical columns, identical primary keys, and the same partial
`idx_path_entries_target` index on non-HIDDEN path entries. Column
meanings are exactly those in §3.2.

There is one deliberate difference. In `volatile.keys` the `volatile`
column defaults to **1** rather than 0, and loregd writes 1 into it. Every
record in this database is volatile by definition, and the column carries
that fact back out in responses without a second lookup.

Because the two schemas are structurally identical, a query that needs
both stores is a `UNION ALL` across them rather than two queries merged
in application code:

```sql
SELECT 1 FROM main.keys WHERE guid = ?
UNION ALL
SELECT 1 FROM volatile.keys WHERE guid = ?
LIMIT 1
```

## 3.3.2 Why this shape matters

Making the volatile store a SQLite database attached to the same
connection buys transactional behaviour for free. A SQLite transaction
spans every attached schema, so a transaction that mutates both
persistent and volatile data commits or rolls back as one unit, with no
separate mechanism to keep the two halves consistent. Volatile writes
inside a transaction are invisible to other readers until commit and
disappear on rollback for the same reason persistent ones do — see §4.3.

## 3.3.3 Lifetime

A shared-cache in-memory database exists as long as at least one
connection has it open. The write connection creates the volatile tables
and holds the database; the read connections and any snapshot connection
attach the same URI and see the same data through the shared cache.

Nothing persists it, and nothing tries to. When loregd exits, the memory
goes with the process and every volatile key in every hive ceases to
exist. That is the entire meaning of volatility here, and it is why the
volatile tables are always empty at startup (§2.2, step 7).

## 3.3.4 Which store an operation uses

For an operation naming a single key, the key's own storage decides:
loregd looks the GUID up in `main.keys` and `volatile.keys`, and whichever
holds it determines where the reads and writes go.

`RSI_CREATE_KEY` is the exception, because the key does not exist yet —
the volatile flag in the request decides which database receives it.

Two operations are not scoped to one store at all. `RSI_LOOKUP` and
`RSI_ENUM_CHILDREN` consult both and combine the results, because a
persistent parent may legitimately have volatile children. The reverse
does not arise: a persistent child beneath a volatile parent is forbidden
by the kernel's data model, so a volatile key's whole subtree is
volatile.

---

# 3.4 Case Folding

_Peios / Advanced Peios / loregd / Storage_

> Names are case-insensitive but case-preserving, implemented by storing both the written form and a folded one.

Key names and value names are case-insensitive but case-preserving. loregd
implements this by storing both forms: the name as written, and a folded
form alongside it in a `_folded` column.

The folded form is computed once, when the name is written, and it is the
folded column that appears in every `WHERE` clause and every primary key.
Lookups are therefore plain binary comparisons:

```sql
SELECT * FROM path_entries
WHERE parent_guid = ? AND child_name_folded = ?
```

This is why no custom SQLite collation is registered anywhere in loregd —
the case-insensitivity has already happened by the time SQLite sees the
query. The canonical name is what comes back in responses, so callers see
the case they originally supplied while storage compares the folded form.

Hive names are folded too, though not stored: routing a request to a hive
and rejecting duplicate hive declarations (§2.1) both compare folded
names.

Layer names are **not** folded. They are compared as binary, so layer
names are case-sensitive.

## 3.4.1 How the folded form is computed

loregd derives the folded form from the Go standard library's
`unicode.ToLower`, with three corrections where lowercasing and simple
case folding disagree:

| Codepoint | Folds to | Why the correction |
|---|---|---|
| U+00B5 MICRO SIGN | U+03BC GREEK SMALL LETTER MU | Lowercasing leaves it unchanged. |
| U+017F LATIN SMALL LETTER LONG S | U+0073 LATIN SMALL LETTER S | Lowercasing leaves it unchanged. |
| U+0130 LATIN CAPITAL LETTER I WITH DOT ABOVE | unchanged | Its folding is a two-codepoint sequence, which simple folding does not produce. |

The standard library's case tables are **Unicode 15.0.0**.

> [!IMPORTANT]
> Lowercasing is not case folding, and the three corrections above do not
> close the gap. 219 codepoints fold to something other than their
> Unicode 16.0 simple case folding, and for 47 of those, case-insensitive
> matching fails outright: two spellings that name the same key do not
> reach the same folded form.
>
> Greek final sigma is the most reachable example. `ς` folds to itself,
> so a key stored as `Σ` is not found by a lookup for `ς`. The Cyrillic
> historic letters at U+1C80–U+1C88 and the Greek symbol variants
> `ϐ ϑ ϕ ϖ ϰ ϱ ϵ` behave the same way. Characters introduced in Unicode
> 16.0 — the Garay block among them — have no case mappings in the 15.0
> tables at all, so their case pairs never match.
>
> The Cherokee blocks fold in the opposite direction to the standard,
> across 172 codepoints. Matching inside a hive stays self-consistent,
> but the bytes written to the folded columns are not what another
> implementation folding the same names would produce.

---

# 4.1 Connections

_Peios / Advanced Peios / loregd / Concurrency_

> Each hive holds a small fixed set of SQLite connections — what each carries, and which one an operation runs on.

Each hive holds a small, fixed set of SQLite connections, and which one an
operation runs on decides both its isolation and what it can block on.

| Connection | Count | Used for |
|---|---|---|
| Write | One per hive | Every mutating operation, and every read inside a bound read-write transaction. |
| Read pool | `min(NumCPU, 16)`, at least 1 | Reads outside a transaction. Selected round-robin. |
| Snapshot | One per active read-only transaction | Reads inside a read-only transaction. Created on demand, closed when the transaction ends. |

The pool size is compiled in and cannot be configured — loregd reads no
configuration (§2.1).

## 4.1.1 One connection per handle

Every one of these is a Go `database/sql` handle limited to a **single
underlying connection**. That single limit is what serialises writes:
there is no separate executor or lock arbitrating the write path, only
the fact that the hive's write handle can hand out one connection at a
time, so a second writer waits for the first to release it.

It also means the snapshot connections are genuinely separate handles
rather than borrowed pool entries, which is what keeps a long-running
read-only transaction from consuming a pool slot.

## 4.1.2 What every connection carries

Connection state is established once, when the connection is opened
(§2.2):

- `journal_mode=wal` on the hive database, verified after being set.
- `foreign_keys=ON`.
- `busy_timeout=25000` — 25 seconds (§4.4).
- The volatile database attached as schema `volatile` (§3.3).

The volatile database's own journal mode is `memory`, not WAL. The
persistent side therefore has multi-version concurrency — readers see a
consistent snapshot and do not block writers — and the volatile side does
not. §4.4 covers what follows from that.

The volatile *tables* are created only on the write connection. Read and
snapshot connections attach the same shared-cache URI and see the tables
through it.

## 4.1.3 Which connection an operation uses

Nine mutating operations are routed to the write connection through the
transaction-aware write path: `RSI_CREATE_KEY`, `RSI_WRITE_KEY`,
`RSI_DROP_KEY`, `RSI_CREATE_ENTRY`, `RSI_HIDE_ENTRY`,
`RSI_DELETE_ENTRY`, `RSI_SET_VALUE`, `RSI_DELETE_VALUE_ENTRY`, and
`RSI_SET_BLANKET_TOMBSTONE`.

Four read operations are routed to the read pool, or to the transaction's
connection when one is bound: `RSI_LOOKUP`, `RSI_ENUM_CHILDREN`,
`RSI_READ_KEY`, and `RSI_QUERY_VALUES`.

`RSI_DELETE_LAYER` and `RSI_FLUSH` take the hive's write connection
directly rather than through the transaction-aware path. They do not
consult the request's transaction id, so they neither join a caller's
transaction nor decline a read-only one, and they open and commit work of
their own.

---

# 4.2 Request Dispatch

_Peios / Advanced Peios / loregd / Concurrency_

> RSI requests arrive multiplexed on one descriptor — identifying the target hive, and the operations that resolve differently.

RSI requests arrive multiplexed on the `/dev/pkm_registry` file
descriptor. Each carries a request id and a transaction id in its header.

loregd reads messages into a single 16 MiB buffer, copies each one out,
and hands it to a **new goroutine** — one per request, with no cap on how
many may be in flight. Responses are serialised by a mutex, because one
write to the device must correspond to exactly one response. On shutdown
the in-flight goroutines are drained before the process exits.

## 4.2.1 Identifying the target hive

Most operations name a key GUID (or, for lookups and enumerations, a
parent GUID), and loregd must decide which hive owns it.

A `guidCache` maps GUID to hive. It is seeded at startup with every
hive's root GUID, and maintained as keys come and go:

- `RSI_CREATE_KEY` stores the new GUID **immediately**, before the
  transaction that created it commits, and registers an abort hook to
  evict it if that transaction rolls back. The immediate store is
  necessary because the cache-miss probe reads through the pool, which
  cannot see uncommitted rows.
- `RSI_DROP_KEY` evicts immediately outside a transaction, or through a
  commit hook inside one.
- `RSI_DELETE_LAYER` evicts the GUIDs its deletion orphaned.

On a miss, loregd probes each hive in turn:

```sql
SELECT 1 FROM main.keys WHERE guid = ?
UNION ALL
SELECT 1 FROM volatile.keys WHERE guid = ?
LIMIT 1
```

Only hits are cached, so a GUID that exists nowhere is re-probed against
every hive on every request that names it. The cache has no size bound;
it shrinks only through the three eviction paths above.

A GUID that resolves to no hive produces `RSI_NOT_FOUND` for most
operations. `RSI_DROP_KEY` and the entry deletions treat it differently —
see §5.3 and §5.4.

## 4.2.2 Operations that resolve differently

`RSI_FLUSH` carries a hive **name** rather than a GUID and resolves it by
folded name (§3.4), so the match is case-insensitive.

`RSI_DELETE_LAYER` does not resolve a hive at all: it applies to every
registered hive and concatenates the orphan sets (§5.6).

---

# 4.3 Transactions

_Peios / Advanced Peios / loregd / Concurrency_

> Transaction ids are allocated by the kernel and bound to connections lazily — beginning, read-write against read-only, committing and aborting.

Transaction identifiers are allocated by the kernel and carried on every
request. loregd binds them to connections lazily — `RSI_BEGIN_TRANSACTION`
does no SQLite work at all.

## 4.3.1 Beginning

`RSI_BEGIN_TRANSACTION` carries the transaction id and a mode:
`RSI_TXN_READ_WRITE` (0) or `RSI_TXN_READ_ONLY` (1). loregd records the id
as pending in the requested mode and returns `RSI_OK` immediately. No
connection is taken and no SQLite transaction is opened.

loregd supports both modes — its SQLite backing provides atomic
read-write commits and stable read-only snapshots — so it never returns
`RSI_TXN_NOT_SUPPORTED`.

Re-using a transaction id that is already active returns `RSI_INVALID`.
If the mode field is absent from the request, the transaction is treated
as read-write.

## 4.3.2 Read-write transactions

The transaction binds to a hive on its **first mutating operation**:
loregd identifies the hive from the operation's GUID, acquires that hive's
write connection, issues `BEGIN IMMEDIATE`, and records the binding. If
SQLite reports `SQLITE_BUSY` at that point, the operation returns
`RSI_TXN_BUSY`.

Once bound, every subsequent operation with that transaction id — reads
included — runs on the same connection. That is what provides
read-your-own-writes: uncommitted rows are visible to the transaction
because it is the connection that wrote them. Reads issued *before* the
transaction binds go to the read pool instead, since there is nothing
uncommitted to see.

Because the connection has both the hive database and the volatile
database attached, a single SQLite transaction spans both. Persistent and
volatile mutations made inside one transaction commit together and roll
back together, with no separate mechanism reconciling them.

An operation whose GUID belongs to a different hive than the transaction
is bound to is rejected with `RSI_STORAGE_ERROR`. The kernel enforces
hive-scoping before requests reach loregd, so this is a backstop.

## 4.3.3 Read-only transactions

A read-only transaction binds on its **first read**. loregd identifies the
hive, opens a **dedicated connection** — deliberately not one from the
read pool, so a long-lived snapshot cannot starve ordinary reads — and
issues `BEGIN DEFERRED`. WAL fixes the snapshot at that first read, and
every later read with the same transaction id reuses the connection and
observes the same point in time.

The snapshot is exact for persistent data. Volatile data has no snapshot
mechanism: a volatile read inside a read-only transaction observes the
live store.

A mutating operation carrying a read-only transaction id is rejected with
`RSI_INVALID` before any state changes — for the nine operations that
route through the write path. `RSI_DELETE_LAYER` and `RSI_FLUSH` do not
check (§4.1).

## 4.3.4 Committing and aborting

`RSI_COMMIT_TRANSACTION` issues `COMMIT` on the bound connection and
returns `RSI_OK`. If the commit fails, the transaction is **left open** so
the caller may retry or abort: a busy or locked failure returns
`RSI_TXN_BUSY`, anything else `RSI_STORAGE_ERROR`. Committing an unknown
transaction id returns `RSI_STORAGE_ERROR`.

Committing a read-only transaction releases the snapshot and returns
`RSI_OK`.

`RSI_ABORT_TRANSACTION` issues `ROLLBACK`, closes the connection, releases
any snapshot, runs the transaction's abort hooks, and always returns
`RSI_OK` — including for a transaction id it has never seen. Rollback
errors are logged and not reported. This is how the kernel releases a
read-only snapshot after a `REG_IOC_BACKUP` finishes or fails.

A transaction that is neither committed nor aborted is never cleaned up:
there is no timeout and no reaper. It holds its hive's write connection
until the process exits — see §4.4.

---

# 4.4 Waiting and Contention

_Peios / Advanced Peios / loregd / Concurrency_

> The busy timeout is set deliberately shorter than the kernel's request timeout — waiting for the write connection, and abandoned transactions.

Every connection is opened with `busy_timeout` set to 25 seconds,
deliberately shorter than the kernel's 30-second request timeout so that
loregd can answer `RSI_TXN_BUSY` before the caller is timed out from
above.

That bound governs one kind of waiting: contention for SQLite's own write
lock on the hive database. Two other kinds arise, and neither is bounded
by it. loregd issues no database operation with a deadline attached.

## 4.4.1 Waiting for the write connection

Each hive's write handle owns exactly one connection (§4.1). When a
read-write transaction binds, it holds that connection until it commits or
aborts, so any other write to the same hive waits — and it waits inside
Go's connection pool, before SQLite is ever reached. `busy_timeout` is not
consulted, because there is no SQLite lock in contention; the second
writer simply has no connection to run on.

`RSI_FLUSH` is the one operation that refuses to join this queue. It
checks whether any transaction is bound to the hive and returns
`RSI_TXN_BUSY` immediately if one is, because a checkpoint on a connection
already held by a transaction would deadlock. The check is racy — a
transaction can bind between the check and the checkpoint. Note also that
it does not distinguish a read-only snapshot, which lives on its own
connection, from a write binding, so a flush during a backup returns
`RSI_TXN_BUSY` even though the checkpoint could have proceeded.

`RSI_DELETE_LAYER`, a non-transactional `RSI_DROP_KEY`, and the
conditional-write path of a non-transactional `RSI_SET_VALUE` all take the
write connection without that guard, and queue behind a bound transaction.

## 4.4.2 Waiting on the volatile store

The volatile database is in shared-cache mode with journal mode `memory`
(§4.1). It therefore has no multi-version concurrency: readers and writers
contend for table locks rather than passing each other.

> [!IMPORTANT]
> A transaction that has written any `volatile.*` table holds a write-table
> lock on it for the transaction's whole lifetime. A volatile read on any
> other connection waits for that lock, and the wait is not bounded by
> `busy_timeout`.
>
> This reaches **every** read operation, not only ones a caller thinks of
> as volatile. All four read operations query `main` and `volatile` in one
> `UNION ALL` statement (§5.1), and hive resolution probes
> `volatile.keys` as well (§4.2). So while a transaction that has touched
> volatile data is open, ordinary reads against the same hive block in
> their serving goroutine rather than returning a status.
>
> The relationship is symmetric. A read-only snapshot that has read a
> volatile table holds a read-table lock for as long as the snapshot
> lives, and volatile writes wait for it. Read-only snapshots are what
> serve the kernel's `REG_IOC_BACKUP`, which is expected to be
> long-lived.

Contention confined to the persistent side behaves as WAL promises: a
transaction writing only the hive database does not block reads of it, and
a transaction writing only volatile tables does not block reads of the
hive database.

## 4.4.3 Abandoned transactions

Nothing reclaims a transaction that is never committed or aborted. There
is no timeout, and no sweep at any point in the daemon's life. Such a
transaction holds its hive's write connection — and, if it wrote volatile
data, its volatile table locks — until the process exits.

---

# 5.1 Store Routing

_Peios / Advanced Peios / loregd / Request Handling_

> Persistent data in the main schema and volatile data in the attached one — how an operation picks, and the ones that span both.

Persistent data lives in the hive database's `main` schema; volatile data
lives in the attached `volatile` schema (§3.3). Most operations act on one
of the two, and loregd has to decide which before it can run any SQL.

## 5.1.1 Operations naming one key

For an operation that names a single key, the key's own `volatile` column
selects the store. loregd reads it with one statement across both schemas:

```sql
SELECT volatile FROM main.keys WHERE guid = ?
UNION ALL
SELECT volatile FROM volatile.keys WHERE guid = ?
LIMIT 1
```

A GUID present in neither produces `RSI_NOT_FOUND` for most operations.

Note that routing follows the **column value**, not which table the row
came from. Rows loregd writes are always consistent about this — a row in
`volatile.keys` carries `volatile = 1`, a row in `main.keys` carries 0 —
so the distinction only matters if a database were modified externally.

`RSI_CREATE_KEY` cannot consult a key that does not exist yet, so it
routes on the volatile flag carried in the request instead.

`RSI_CREATE_ENTRY` routes on the volatile flag of the **child** key the
entry points at. When that child GUID is present in neither store, the
entry is written to the persistent table.

`RSI_HIDE_ENTRY` routes on the **parent** key, since a HIDDEN entry
belongs to the parent's child list and a volatile parent's whole subtree
is volatile.

## 5.1.2 Operations spanning both stores

`RSI_LOOKUP`, `RSI_ENUM_CHILDREN`, `RSI_READ_KEY` and `RSI_QUERY_VALUES`
are not scoped to one store: a persistent parent may have volatile
children, and a persistent key may have volatile-store rows beneath it.
Each issues a single `UNION ALL` statement over the two schemas rather
than querying them separately, so the merge happens inside SQLite.

Nothing de-duplicates across the two stores. The primary keys that make
`(parent_guid, child_name_folded, layer)` unique apply *per schema*, so if
the same triple exists in both, both rows appear in the response. The same
holds for value entries keyed on `(key_guid, name_folded, layer)`.

The deletions — `RSI_DELETE_ENTRY`, `RSI_DELETE_VALUE_ENTRY` and
`RSI_DROP_KEY` — do not route at all. They delete from both schemas
unconditionally.

---

# 5.2 Response Ordering

_Peios / Advanced Peios / loregd / Request Handling_

> The queries behind enumerations carry no ORDER BY, so their order is not stable across calls — and what that means for the kernel.

The queries backing enumerations and lookups carry no `ORDER BY`, and a
`UNION ALL` across the two stores yields rows in whatever order SQLite
produces them. That order is not stable across calls.

The kernel walks enumeration results by dense index across repeated
calls, so an unstable order would make that walk duplicate or drop
entries. loregd therefore sorts every affected array into a canonical
order before encoding a response:

| Response | Sorted by |
|---|---|
| `RSI_LOOKUP` path entries | layer, then sequence |
| `RSI_ENUM_CHILDREN` children | folded child name |
| `RSI_ENUM_CHILDREN` per-child entries | layer, then sequence |
| `RSI_QUERY_VALUES` value entries | folded value name, then layer, then sequence |
| Key-metadata blocks, in any response | ascending GUID, compared bytewise |

This ordering is a wire-stability guarantee only. It has no bearing on
layer resolution, which is order-independent — the kernel selects a
maximum, not a first match.

## 5.2.1 Arrays that are not sorted

Two arrays reach the wire in the order the query produced them:

- The **blanket-tombstone array** in an `RSI_QUERY_VALUES` response. It
  comes from an unordered `UNION ALL` like everything else, but is
  emitted unsorted.
- The **orphan-GUID array** in an `RSI_DELETE_LAYER` response. That
  operation walks every registered hive and concatenates their orphan
  sets, and the walk follows Go's randomised map iteration, so the array
  order differs between otherwise identical calls.

## 5.2.2 Child display names

`RSI_ENUM_CHILDREN` groups rows by `child_name_folded` and emits one
child block per folded name, carrying a display name taken from the
`child_name` column.

Where two rows share a folded name but differ in stored case — `Foo` in
one store and `FOO` in the other, say — the display name emitted is
whichever row the unordered union yielded first. The *order* of children
is stable, because it is sorted on the folded name; the *case* of the name
reported for such a child is not.

---

# 5.3 Key Operations

_Peios / Advanced Peios / loregd / Request Handling_

> The four key operations and the SQL behind each — create, read, write and drop.

## 5.3.1 RSI_CREATE_KEY

The request's volatile flag selects the target table (§5.1). For a
persistent key:

```sql
INSERT INTO keys
    (guid, name, name_folded, parent_guid, sd, volatile, symlink,
     last_write_time)
VALUES (?, ?, fold(?), ?, ?, 0, ?, ?)
```

`last_write_time` is not carried in the request. loregd sets it to the
current wall-clock time in Unix nanoseconds at insertion.

Uniqueness comes from the target table's primary key on `guid`, surfaced
as `RSI_ALREADY_EXISTS`. Because that key is per-schema, a GUID already
present in the *other* store does not collide: creating a persistent key
whose GUID exists in `volatile.keys` succeeds, and the GUID then exists in
both. Subsequent metadata reads resolve such a GUID to the `main` row,
since the reading query takes the first row of a `UNION ALL` that puts
`main` first.

The new GUID is added to the hive cache immediately, before the enclosing
transaction commits, with an abort hook to remove it if that transaction
rolls back (§4.2).

An unresolvable parent GUID returns `RSI_NOT_FOUND`, after a fallback
check of the registered hives' root GUIDs.

## 5.3.2 RSI_READ_KEY

```sql
SELECT name, parent_guid, sd, volatile, symlink, last_write_time
FROM main.keys WHERE guid = ?
UNION ALL
SELECT name, parent_guid, sd, volatile, symlink, last_write_time
FROM volatile.keys WHERE guid = ?
LIMIT 1
```

`RSI_NOT_FOUND` if the GUID is in neither store, and likewise if it
resolves to no hive. The `volatile` field in the response is the stored
column value.

## 5.3.3 RSI_WRITE_KEY

Updates the two mutable fields of a key, selected by a field mask:

| Bit | Value | Field |
|---|---|---|
| 0 | `0x01` | `sd` |
| 1 | `0x02` | `last_write_time` |

Valid masks are therefore `0x00`, `0x01`, `0x02` and `0x03`. Any other bit
set returns `RSI_INVALID` — it indicates an attempt to modify an immutable
field.

loregd builds one `UPDATE` from the mask, setting only the named fields:

```sql
-- mask 0x03
UPDATE keys SET sd = ?, last_write_time = ? WHERE guid = ?
```

A mask of `0x00` names no fields and acts as an existence check, returning
`RSI_OK` or `RSI_NOT_FOUND`. An update that matches no row also returns
`RSI_NOT_FOUND`.

Outside a transaction the update is a single auto-committed statement.
Inside one it runs on the transaction's connection.

## 5.3.4 RSI_DROP_KEY

Purges every trace of a GUID from both stores — four tables in each
schema:

```sql
DELETE FROM keys               WHERE guid = ?;
DELETE FROM path_entries       WHERE target_guid = ?;
DELETE FROM [values]           WHERE key_guid = ?;
DELETE FROM blanket_tombstones WHERE key_guid = ?;
```

Outside a transaction, the eight statements are wrapped in a
`BEGIN IMMEDIATE` transaction of their own so the purge is atomic. Inside
one, they run on the transaction's connection.

Dropping a GUID that does not exist returns `RSI_OK`; so does one that
resolves to no hive. The operation is idempotent. The GUID is evicted from
the hive cache (§4.2).

---

# 5.4 Path Entry Operations

_Peios / Advanced Peios / loregd / Request Handling_

> Lookup, create, hide, delete and enumerate — the operations over the per-layer entries that give a key its names.

## 5.4.1 RSI_LOOKUP

Returns every layer's entry for one child name under one parent, together
with metadata for the keys those entries point at.

```sql
SELECT layer, target_type, target_guid, sequence
FROM main.path_entries
WHERE parent_guid = ? AND child_name_folded = ?
UNION ALL
SELECT layer, target_type, target_guid, sequence
FROM volatile.path_entries
WHERE parent_guid = ? AND child_name_folded = ?
```

HIDDEN entries are returned as entries but contribute no metadata GUID.
For each distinct non-HIDDEN `target_guid`, loregd fetches the key's
metadata — one query per GUID — and emits the blocks in ascending GUID
order (§5.2).

loregd does no layer filtering and no resolution: every entry it holds is
returned, and choosing between them is the kernel's job.

An unresolvable parent GUID returns `RSI_NOT_FOUND`.

If a path entry names a `target_guid` for which no key record exists, the
metadata fetch finds nothing and the whole request fails with
`RSI_STORAGE_ERROR`. This is reachable in ordinary operation: the kernel
issues `RSI_CREATE_ENTRY` before `RSI_CREATE_KEY`, so a lookup landing
between the two sees an entry whose key has not yet been written.

## 5.4.2 RSI_CREATE_ENTRY

```sql
INSERT INTO path_entries
    (parent_guid, child_name, child_name_folded, layer,
     target_type, target_guid, sequence)
VALUES (?, ?, fold(?), ?, 0, ?, ?)
```

The target table follows the child key's volatile flag (§5.1); a child GUID
in neither store lands in the persistent table.

`RSI_ALREADY_EXISTS` comes from the target table's primary key on
`(parent_guid, child_name_folded, layer)`. Since that key is per-schema, an
identical entry in the *other* store does not collide, and the same triple
can come to exist in both — in which case both rows are returned by lookups
and enumerations (§5.1).

An unresolvable parent GUID returns `RSI_NOT_FOUND`.

## 5.4.3 RSI_HIDE_ENTRY

Writes a tombstone that masks the same name in lower layers:

```sql
INSERT OR REPLACE INTO path_entries
    (parent_guid, child_name, child_name_folded, layer,
     target_type, target_guid, sequence)
VALUES (?, ?, fold(?), ?, 1, NULL, ?)
```

`target_type` is 1 and `target_guid` is null. The target table follows the
**parent** key's volatile flag, because a volatile parent's entire subtree
is volatile. A parent GUID in neither store returns `RSI_NOT_FOUND`.

## 5.4.4 RSI_DELETE_ENTRY

Removes one layer's entry for one name, from both stores:

```sql
DELETE FROM path_entries
WHERE parent_guid = ? AND child_name_folded = ? AND layer = ?
```

No rows-affected check is made, so deleting an entry that is not there
succeeds. A parent GUID that resolves to no hive returns
`RSI_NOT_FOUND` rather than succeeding.

## 5.4.5 RSI_ENUM_CHILDREN

Returns every layer's entry for every child under a parent:

```sql
SELECT child_name, child_name_folded, layer, target_type,
       target_guid, sequence
FROM main.path_entries WHERE parent_guid = ?
UNION ALL
SELECT child_name, child_name_folded, layer, target_type,
       target_guid, sequence
FROM volatile.path_entries WHERE parent_guid = ?
```

Rows are grouped by folded child name into one block per child, each
carrying that child's per-layer entries. Metadata for the distinct
non-HIDDEN target GUIDs is fetched and emitted exactly as for
`RSI_LOOKUP`, and the same `RSI_STORAGE_ERROR` arises for an entry whose
key record does not yet exist.

Ordering, and the treatment of two rows whose folded names match but whose
stored case differs, are covered in §5.2.

---

# 5.5 Value Operations

_Peios / Advanced Peios / loregd / Request Handling_

> Querying, setting and deleting value entries, conditional writes, and setting a blanket tombstone.

## 5.5.1 RSI_QUERY_VALUES

Returns every layer's entry for one value, or for all of a key's values
when the request sets the query-all flag:

```sql
-- single value
SELECT name, layer, type, data, sequence
FROM main.[values] WHERE key_guid = ? AND name_folded = ?
UNION ALL
SELECT name, layer, type, data, sequence
FROM volatile.[values] WHERE key_guid = ? AND name_folded = ?

-- query all
SELECT name, layer, type, data, sequence
FROM main.[values] WHERE key_guid = ?
UNION ALL
SELECT name, layer, type, data, sequence
FROM volatile.[values] WHERE key_guid = ?
```

The response also carries the key's blanket-tombstone state:

```sql
SELECT layer, sequence
FROM main.blanket_tombstones WHERE key_guid = ?
UNION ALL
SELECT layer, sequence
FROM volatile.blanket_tombstones WHERE key_guid = ?
```

Value entries are sorted; blanket tombstones are not (§5.2).

An unresolvable GUID returns `RSI_NOT_FOUND`. An existing key with no
values returns `RSI_OK` with empty arrays.

## 5.5.2 RSI_SET_VALUE

The key's volatile flag selects the store; a key GUID in neither store
returns `RSI_NOT_FOUND`.

```sql
INSERT OR REPLACE INTO [values]
    (key_guid, name, name_folded, layer, type, data, sequence)
VALUES (?, ?, fold(?), ?, ?, ?, ?)
```

### 5.5.2.1 Conditional writes

When the request carries a non-zero `expected_sequence`, the write is a
compare-and-swap. loregd reads the current entry's sequence and writes only
if it matches:

```sql
SELECT sequence FROM [values]
WHERE key_guid = ? AND name_folded = ? AND layer = ?
```

If the row is absent, or its sequence differs, the operation returns
`RSI_CAS_FAILED` and writes nothing.

Outside a transaction, the check and the write are wrapped in their own
`BEGIN IMMEDIATE` transaction so no other writer can interleave; if that
transaction cannot begin because the database is busy, the operation
returns `RSI_TXN_BUSY`. Inside a transaction, the caller's transaction
already provides the isolation.

## 5.5.3 RSI_DELETE_VALUE_ENTRY

Removes one layer's entry for one value, from both stores:

```sql
DELETE FROM [values]
WHERE key_guid = ? AND name_folded = ? AND layer = ?
```

No rows-affected check, so deleting an absent entry succeeds. A GUID that
resolves to no hive returns `RSI_NOT_FOUND`.

## 5.5.4 RSI_SET_BLANKET_TOMBSTONE

Sets or clears the tombstone that masks every value a key holds in lower
layers. The key's volatile flag selects the store, and a GUID in neither
returns `RSI_NOT_FOUND`.

```sql
-- set
INSERT OR REPLACE INTO blanket_tombstones (key_guid, layer, sequence)
VALUES (?, ?, ?)

-- clear
DELETE FROM blanket_tombstones WHERE key_guid = ? AND layer = ?
```

---

# 5.6 Layer and Maintenance Operations

_Peios / Advanced Peios / loregd / Request Handling_

> Delete-layer and flush — the two operations that ignore the request's transaction id and take the write connection directly.

Neither operation in this section consults the request's transaction id
(§4.1). Both take the hive's write connection directly and commit work of
their own.

## 5.6.1 RSI_DELETE_LAYER

Removes every entry belonging to one layer and reports the keys that the
removal left unreferenced.

The operation is applied to **every registered hive**, not to one
identified from the request, and the per-hive orphan sets are
concatenated into a single response array.

For each hive, inside one `BEGIN IMMEDIATE` transaction, loregd first
computes the orphan set and then deletes:

```sql
-- GUIDs referenced by the layer being removed
SELECT DISTINCT target_guid FROM main.path_entries
WHERE layer = ? AND target_type = 0
UNION
SELECT DISTINCT target_guid FROM volatile.path_entries
WHERE layer = ? AND target_type = 0
```

minus the GUIDs referenced by any *other* layer, gathered the same way
with `layer != ?`. What remains is reachable only through the layer being
deleted, and is therefore orphaned by it.

```sql
DELETE FROM path_entries       WHERE layer = ?;
DELETE FROM [values]           WHERE layer = ?;
DELETE FROM blanket_tombstones WHERE layer = ?;
```

Both schemas are covered, and all six deletions run inside the same
transaction as the orphan computation, so nothing can be inserted between
the two steps. The orphaned GUIDs are evicted from the hive cache (§4.2)
and returned to the caller.

The response array's order is not stable across calls (§5.2).

Every failure is reported as `RSI_STORAGE_ERROR`; busy errors are not
classified separately, unlike the other write paths.

## 5.6.2 RSI_FLUSH

Forces the hive's write-ahead log to be checkpointed so that all
persistent data is durable on disk:

```sql
PRAGMA wal_checkpoint(TRUNCATE)
```

The request carries a hive **name** rather than a GUID. It is matched
case-insensitively against the registered hives by folded name (§3.4); a
name matching none returns `RSI_INVALID`.

Two conditions return `RSI_TXN_BUSY` instead of checkpointing:

- Any transaction is currently bound to the hive. A checkpoint on a
  connection already held by a transaction would deadlock, so loregd
  declines immediately rather than waiting (§4.4).
- The checkpoint itself reports that it could not complete because the
  database was busy.

The volatile store has no durability and is unaffected: nothing is
flushed, and nothing needs to be.

---

# 5.7 Status Codes

_Peios / Advanced Peios / loregd / Request Handling_

> The seven RSI status codes loregd returns, and the three the interface defines that it never produces.

loregd returns seven of the RSI status codes:

| Status | Value | Returned when |
|---|---|---|
| `RSI_OK` | 0 | The operation succeeded. Also returned by the idempotent deletions when their target was already absent. |
| `RSI_NOT_FOUND` | 1 | A named key GUID exists in neither store, or resolves to no registered hive. Also an update matching no row. |
| `RSI_ALREADY_EXISTS` | 2 | A primary-key collision in the target table — a duplicate key GUID, path entry, or value entry within one schema. |
| `RSI_STORAGE_ERROR` | 3 | A SQLite failure while serving the request. Also an operation whose GUID belongs to a hive other than the one its transaction is bound to, a mutating operation carrying an unknown transaction id, a failed commit that was not busy, and any `RSI_DELETE_LAYER` failure. |
| `RSI_TXN_BUSY` | 6 | `BEGIN IMMEDIATE` found the database busy, a conditional write could not begin its transaction, or `RSI_FLUSH` found a transaction bound to the hive. |
| `RSI_INVALID` | 7 | An unknown opcode, a request whose payload cannot be decoded, an out-of-range `RSI_WRITE_KEY` field mask, a mutating operation carrying a read-only transaction id, an `RSI_FLUSH` naming an unregistered hive, or an `RSI_BEGIN_TRANSACTION` re-using an active id. |
| `RSI_CAS_FAILED` | 8 | A conditional `RSI_SET_VALUE` whose target was absent or whose sequence did not match. |

Three codes are defined by the interface and never produced by loregd:
`RSI_NOT_EMPTY` (4), `RSI_TOO_LARGE` (5), and `RSI_TXN_NOT_SUPPORTED` (9).

`RSI_TXN_NOT_SUPPORTED` is never needed because loregd supports both
transaction modes (§4.3).

`RSI_TOO_LARGE` is never returned because an oversized or malformed frame
is not answered at all. Framing is validated before an opcode is known, and
a frame that fails validation ends the connection instead of producing a
response (§2.3).

---

# Appendix A Prior Art

_Peios / Advanced Peios / loregd_

> loregd is specified against SQLite rather than an abstract store — how it relates to SQLite, the Windows hive format, and RSI.

## A.1 SQLite

loregd's storage engine is SQLite, and it is specified against SQLite
rather than against an abstract store. The schema, the concurrency model,
and the operational behaviour all name SQLite features directly: WAL mode
for concurrent readers alongside a serialised writer, savepoints and
transactions for atomicity, `PRAGMA wal_checkpoint` for durability on
demand, and its crash recovery for restart after an unclean shutdown.

Both halves of a hive are SQLite. Persistent data lives in the database
file named on the command line; volatile data lives in a second,
in-memory database attached to the same connections (§3.3). Using one
engine for both is what makes a transaction spanning persistent and
volatile data atomic without any additional machinery.

A different storage engine would have to supply equivalent transactional
and concurrency semantics. Nothing in the design forbids that, but
nothing accommodates it either.

## A.2 The Windows registry hive format

The Windows registry stores hives as binary REGF files managed directly
by the kernel's Configuration Manager. loregd departs from that model
completely: storage is a SQLite database managed by an unprivileged
userspace daemon, not a kernel-managed binary file, and the kernel holds
no storage of its own at all.

What the two share is the data model — keys, values, security descriptors
— which Peios inherits through the registry's kernel-side specification
rather than from the file format. The on-disk format has no relationship
to REGF whatsoever, and no REGF file can be read by loregd or written by
it.

## A.3 The Registry Source Interface

loregd implements the RSI, which is
[specified in PSPK](/peios/advanced-peios/pspk/registry-source-interface/scope.md) rather than
here. That specification defines the operations, the message format, the
error model, and the obligations binding on any source; loregd's request
handling (chapter 5) is a mapping of those operations onto SQL against
the two stores.

Where this manual and the RSI specification disagree about wire
behaviour, the specification is correct and this manual has a bug — the
RSI is a contract with the kernel, and loregd is one implementation of
one side of it.
