Tags: dx, event-stream, porcelain, developer-contract
Applications use Lace to create records, select them with policy, and exchange them with other participants. They should not need to drive protocol phases or inspect store internals to do so.
This specification defines the shared application contract for Rust, JavaScript, Python, and C: records, participants, operations, observations, and outcomes. Bindings preserve ownership and protocol guarantees using their native call, future, thread, cancellation, and destruction models; identical timing and signatures are not required. Specs 010–065 remain authoritative for the underlying protocol.
A validated Record exposes its typed hash, kind
(Blob, Plex, or Seal), opaque
data bytes, and ordered application field occurrences. Blobs carry data;
Plex records add fields; Seals add Mark evidence over a Plex. Looking up
a field name returns all its values in canonical occurrence order, never
silently one value. Iteration returns every application occurrence in
canonical record order. Record-link access returns the parsed links
defined by 020. Seals also expose canonical By and
Mark text.
Structural fields Data-Length, By, and
Mark and synthetic fields :Type and
:Length belong to the 020 fact or projection surface, not
application-field iteration. All bindings expose the same information
and occurrence order through their ordinary byte, string, iterator, and
lifetime forms.
Record metadata (hash, kind, lengths, fields, links, By
and Mark) is always in memory; the canonical bytes need not
be. A record exposes its canonical length and data offset, a bounded
read of canonical bytes at an offset, and a contiguous view only where
it holds its bytes contiguously, such as a constructed, parsed,
in-memory, or mapped record. Materialising the whole record or its data
is an explicit call with a caller-supplied size limit that fails when
the bytes exceed it. No binding promises a complete slice or reads
storage behind a metadata accessor.
| Surface | Length | Bounded read | Contiguous view | Materialise |
|---|---|---|---|---|
| Rust | canonical_len() |
read_at(offset, buffer) |
contiguous_bytes(), contiguous_data() |
to_vec(limit), data_to_vec(limit) |
| JavaScript | canonicalLength |
readAt(offset, length) |
contiguousBytes(), contiguousData() |
toBytes(limit), dataToBytes(limit) |
| Python | canonical_len |
read_at(offset, size) |
contiguous_bytes(), contiguous_data() |
to_bytes(limit), data_to_bytes(limit) |
| C | lace_record_canonical_len |
lace_record_read_at |
lace_record_contiguous_ptr |
— |
A record is owned independently of a store snapshot or protocol machine. No runtime adds a required field, singleton field, application schema, or mutable row identity. Applications define field meanings; policy defines record subsets. Interlace converges the records selected by both sides.
A constructor accepts optional data, an ordered sequence of
(name, value) occurrences, and an optional private
By-secret value. It selects the form exactly as follows:
empty specification -> Blob with empty data
data, no fields, no By-secret -> Blob
one or more fields, no By-secret -> Plex
By-secret with no fields -> error
By-secret with one or more fields -> Seal over Plex
Omitted data means empty bytes. Constructors never manufacture fields, timestamps, identifiers, scopes, or placeholders. String data convenience input is UTF-8; binary APIs accept the language’s ordinary byte value. Constructors apply 010 validation and sorting, preserving caller order among same-name occurrences and rejecting reserved names or invalid names and values.
| Surface | Data | Fields | Marking input |
|---|---|---|---|
| Rust | byte value | ordered (name, value) sequence |
BySecret |
| JavaScript | byte value or UTF-8 string convenience | ordered pairs | bySecret |
| Python | bytes or UTF-8 string convenience | ordered pairs | by_secret |
| C | builder data setter | repeated field append | By-secret setter |
Rust uses ByValue for canonical public
V.<b64a>.H3 text and one zeroizing
BySecret owner for canonical private
&.<b64a>.H3 text.
BySecret::by_value() derives the public value;
Record::by() returns it only for a Seal. JavaScript,
Python, C, facts, and wire surfaces use the same canonical public text,
not raw key bytes or another public-key representation. Public
By text is output, never a substitute for the private
marking input. Rust BySecret::from_secret_key and
JavaScript bySecretFromBytes build the private text from an
application-supplied 32-byte payload, whose secrecy is the application’s
(009).
Construction returns a validated owned Record. Parsing
canonical bytes is a separate validating protocol boundary, not another
construction specification.
Reading a 010
record stream yields its records in stream order, each once its
bytes are validated. A record is received as an ILTP response would be:
into the given Lace’s record staging when its store has staging,
otherwise into memory. Reading admits nothing; store or
store_stream offers the records to a target. A staged
record holds its staging file until the record drops.
The reader takes a byte bound for the whole stream. A record whose
canonical length exceeds what remains of it fails with
LimitExceeded at its start, before any of its bytes are
held. An invalid record, bytes after a record that do not form one, or a
stream ending inside a record fail with PeerProtocol; a
failed read is Transport and a failed staging write
Store. A failure ends the stream and discards the partial
record and its staging. An empty stream yields nothing.
| Surface | Reader |
|---|---|
Rust lace-rt |
read_record_stream(&handle, impl Read, max_bytes) -> RecordStream,
an iterator of Result<Record> |
Rust lace-tokio |
read_record_stream(&handle, impl AsyncRead, max_bytes) -> RecordStream,
next().await |
| JavaScript | readRecordStream(lace, ReadableStream<Uint8Array>, { maxBytes }),
an async iterable of Record |
| Python | read_record_stream(lace, reader, *, max_bytes), an
iterator of Record over a binary reader |
A Lace owns an adopted store and its lifecycle. A
clonable opaque LaceHandle gives operations access to that
participant without exposing the store. A clonable
LocalPeer pairs a handle with its own prepared policy
operand: Rust LocalPeer::new(handle, policy), JavaScript
new LocalPeer(lace, policy), Python
LocalPeer(lace, policy); preparation failure is a
construction error. A ConnectedPeer instead owns
host-established transport IO and is claimed by one operation.
Direct interlace joins two local participants; connected interlace
joins a Lace or LaceHandle and a connected
peer. A bounded operation returns a settled outcome; an open operation
returns a handle for continued observation. The ordinary surface is:
open_interlace / openInterlace(first, second, policy) -> open handle
bounded_interlace / boundedInterlace(first, second, policy) -> bounded outcome
list(target, policy, options) -> ListOutcome
get(target, policy, options) -> GetOutcome
store(target, records, options) -> StoreOutcome
store_stream / storeStream(target, options) -> offer stream
for_each / forEach(target, policy, options) -> observation stream
The convenience operations, called porcelain, create private
participants and run ordinary interlace. Public record access always
follows policy, exposure, and interlace; it does not bypass them through
raw database access or remote commands. The signatures above describe
operation roles, not a common calling convention. JavaScript uses
openInterlace(first, second, options) and
boundedInterlace(first, second, options), with
policy inside options; list,
get, and forEach take positional policy
followed by options. store takes records followed by
options and no caller policy. JavaScript open setup returns a promise
for its handle; it does not await peer establishment or fixed point.
Rust synchronous and Python opening remain synchronous; Tokio uses a
future. Record collections follow each binding’s native input forms.
Each operation retains a handle for every local participant position.
Both positions may refer to the same Lace without changing behavior or
outcomes. Bounded interlace and list, get, and
store return outcomes, not participant handles. Callers
retain or clone handles beforehand when they need reusable local access.
A claimed connected participant is consumed and settled by a bounded
operation. Neither Lace, LaceHandle, nor
LocalPeer exposes a raw store, socket, transaction, or
protocol machine.
The ordinary string policy is anonymous Interlang:
converge:
include Record:
Record['Content-Type'] == 'text/plain'
This selects records with exactly one Content-Type
occurrence equal to text/plain. A bounded format test uses,
for example, Record.Name matches '[a-z][a-z0-9-]{0,31}';
see 120 operator tests
and 030
TextMatch. It uses the existing policy APIs, not a
separate matching API. Operations also accept prepared canonical
operands. Source conveniences accept one self-contained contribution.
Compile independent contributions separately and compose their prepared
policies as described under Preparing policies.
converge selects stored records, generates request
discovery, and implies whole-record exposure. expose alone
permits peer-origin inspection without transfer. A record leaves a side
only when that side’s policy both selects and exposes it, and the peer’s
policy also selects it. There is no implicit select-all source; advanced
Datalog can use explicit Have. Every binding publishes the
prepared all-records policy, which selects, exposes, and requests every
record: Rust policy::all_records(), JavaScript
allRecords(), Python all_records(), and C
lace_policy_all_records().
Ordinary interlace is duplex. Its policy argument is an operand
template, not a merged executable or exchange plan. In a direct
operation, a bare Lace or LaceHandle position
receives it and a LocalPeer position receives its own
operand, in argument slot order. At most one position may be a
LocalPeer, and a LocalPeer cannot join a
ConnectedPeer; either violation is a Policy
call error. Porcelain accepts a LocalPeer only as its
target. In a connected operation, the local participant contributes one
operand and the peer supplies the other as its role-assigned operand,
which crosses as a Blob only when the local Lace does not hold it. An
operand may derive TxDisabled/0 or
RxDisabled/0; direction is policy, not a separate
option.
Both APIs reach the same 050 fixed point: neither side requested another record in the settled round. A bounded operation returns after required final output and raw transport release. An open operation waits on each local Lace’s retained position-or-terminal-failure signal and may start another round after local store progress.
Protocol fixed point is not yet a successful public outcome: the
channel owner must accept required output and the route must settle. The
output batch accepted at the transition into fixed point, the settled
round’s final request block, retains its original absolute phase
deadline until fully written and flushed to the adopted IO. Neither API
publishes FixedPoint before that boundary; bounded
completion also waits for route settlement as 050
defines. A connected route settles once the local writer has shut down
and the peer’s direction has ended; an end-mark write or writer shutdown
failure still settles when the peer’s direction ended cleanly.
Operation options are the five local, unnegotiated 050 lifecycle
settings: received control bytes, transferred record bytes, evaluation
work, peer operand bytes, and phase timeout. All but the phase timeout
produce LimitExceeded; phase expiry produces
Timeout. Parser/wire caps, transport buffers,
store-position discovery, and polling cadence are not operation
options.
A runtime may expose physical IO configuration: the size of the chunk
buffers that carry transport bytes, stream encodings, file-range record
reads and store copies, and a budget those buffers reserve from. Rust
lace-rt and lace-tokio each set both
process-wide for their own runtime with
configure_io(IoSettings { chunk_bytes, budget_bytes });
work set up afterwards uses them. A connected operation reserves its
chunk buffers during setup, a record stream reader its read buffer, and
a store that stages received records its copy buffer during adoption, so
an exhausted budget is a LimitExceeded call error before
driving; reservations return when the operation’s IO is released, the
reader drops or the store is closed. Records, direct operations and
in-memory record assembly are not counted.
Bindings expose one tagged observation union:
RecordSent { kind, position, record_hash }
RecordSaved { kind, position, record_hash, optional owned Record }
FixedPoint { kind }
Closed { kind, reason }
RecordSent reports that a provider emitted a record
response.RecordSaved reports settled local admission of a
received record. It stays with the participant that admitted it; it is
not a remote acknowledgement.FixedPoint reports each fixed-point entry once and
leaves an open stream open.Closed reports clean peer or local closure where the
API exposes lifecycle. Its reasons are PeerClosed and
Cancelled.Only record observations carry first or
second, matching application argument position
(ParticipantPosition::First and Second in
Rust). This identifies neither protocol side nor authority, locality,
initiator status, or durable receipt. FixedPoint and
Closed are operation-wide. Open streams, including
for_each, attach the owned Record to
RecordSaved; bounded outcomes report only its hash. Round
numbers, fixed-point identities, store positions, raw binding
dictionaries, and protocol phases remain diagnostics, not required
observation fields.
Every host exposes one-event reading as the canonical open-operation
primitive. Iteration delegates to it. A run-to-fixed-point helper
returns every observation it consumed through FixedPoint or
Closed; it neither silently discards them nor runs a second
driver. Result collectors use these same observations and fixed-point
state, not additional protocol event families.
Clean closure yields Closed once, then exhaustion.
Terminal failure returns one typed error, without a synthetic event,
then exhausts. The failed handle retains that error for later methods
such as policy replacement. Queues and transport buffers remain bounded:
a consumer that stops reading causes backpressure, cancellation, or
closure rather than unbounded growth.
Rust open and observer handles also expose consuming
close(self) (async in Tokio). It settles
started work and connected transport ownership, then returns the final
Closed(Cancelled) boundary or observer event. Drop is a
non-waitable resource-safety fallback.
Explicit close MUST attempt all owned cleanup and finish required
started work before reporting its result. An existing operation or
participant failure remains visible; a distinct cleanup failure MUST NOT
replace or disappear behind it. Bindings preserve the primary failure
and additional failures through their existing error channel, not a
cleanup event or separate status API. JavaScript uses
AggregateError when failures combine, retaining original
reason values; typed projections MUST preserve that group as a cause
rather than flattening it to message text. After driving starts, bounded
wrappers preserve partial results and use a structured failed terminal
for cleanup failure rather than successful completion or a
stopped-by-limit terminal that hides the failure. Failed setup and
empty-store disposal errors remain call errors because driving never
started.
Where close is repeatable, concurrent and later calls observe the
same pending or settled result. A replayed retained failure is not
another cleanup failure. For JavaScript, cancellation/failure presence
MUST be tracked independently of the reason’s truthiness;
false, 0, '', null,
and NaN remain exact rejection values when no additional
failure combines with them. Both pending and newly issued event reads
MUST respect cancellation settlement before reporting its result; queued
success or exhaustion cannot bypass that boundary.
Settled cleanup means required attempts and owned work finished, not that arbitrary IO implementations physically released everything or that a peer acknowledged retirement. Error identity supplies no additional cleanup-status guarantee.
Except for caller cancellation, a bounded operation returns exactly one terminal:
The outcome owns its terminal and ordered observations. List, get, and store outcomes also own records or per-input results. Only failed carries an error; stopped-by-limit has no error payload. Caller cancellation uses the language-native call cancellation path after cleanup, not a bounded terminal.
Argument validation, policy preparation, participant claiming, route setup, and driver construction precede operation start. Failures there are typed call errors. Once driving starts, failures become outcome terminals and preserve prior observations.
A complete operation collects record observations in production
order, settles obligations, final output, and its route, appends
FixedPoint, then returns. A limit or failure returns prior
observations without FixedPoint or an error observation.
Bounded teardown does not append Closed. Empty
store is the explicit exception: it completes without
interlace or any observations.
Only a bounded wrapper maps core LimitExceeded to
stopped-by-limit. Phase expiry is failure; scheduling
yields and would-block states are not outcomes. Boundary EOF is complete
only at the 050 fixed point; premature semantic EOF is failure. Open
streams end by cancellation, closure, or failure, not a bounded
completion status.
Rust, C, WebAssembly, JavaScript, and Python share these categories:
PeerProtocol, LimitExceeded,
Timeout, Policy, Store,
Transport, and Integration. A structured error
carries its category, a human-readable message, and optional participant
position and record hash. Only Timeout carries a stable
phase: Prefacing, Side0Proof,
Side1Proof, Side0Operand,
Side1Operand, Advertising,
Requesting, Answering, or
Fencing. Callers need not parse debug text. Errors are not
observations.
Malformed, truncated, oversized, or noncanonical incoming framing is
PeerProtocol; local encoder or construction-invariant
failure is Integration. A store refusing an admission at
its capacity is LimitExceeded; every other store failure is
Store. Connected local failure closes transport. The peer
may see transport failure, framing truncation, or premature EOF rather
than the local error details.
list and
get capture selected recordslist creates a private capture Lace and returns only
records validated and admitted there, collected from ordinary
RecordSaved observations. Records captured before a limit
or failure remain in the outcome; its terminal indicates that the result
is not exhaustive. ILTP defines no result order. An API promising order
defines it separately.
get captures one result: the first record admitted to
its private capture Lace. No recency rule changes that choice. The
result survives a later limit or failure, but the operation still
attempts to settle outstanding obligations and mandatory follow-up
rounds. Empty selection completes normally.
store offers the
supplied recordsstore creates a provider Lace containing exactly the
supplied validated records. The target’s policy decides what to request
and persist; a bare local target, which has no operation policy, uses
the all-records operand, and a LocalPeer target uses its
own operand. Empty input is a complete no-op: no interlace, record
results, observations, or synthetic FixedPoint. It still
consumes and releases an accepted connected input under the binding’s
ownership rules below. Explicit cleanup failure MUST NOT be reported as
successful no-op completion.
For nonempty input, each unique record receives one route-neutral disposition:
| Disposition | Meaning |
|---|---|
| Saved | the provider emitted RecordSent and the bounded
operation returned complete |
| Skipped | the operation returned complete without emitting that record |
| Unsettled | the operation did not return complete, whether or not it emitted the record |
An internal fixed-point transition followed by final-output or
route-settlement failure, including output failure the channel owner
reports at shutdown, does not report Saved.
RecordSent followed by fixed point confirms settled target
admission, including a duplicate admission after a frozen request. It
does not promise a new physical insertion, filesystem flush, or
independently attested persistence, nor vouch for a nonconforming
peer.
Skipped deliberately leaves already-held and
policy-declined indistinguishable on direct and connected routes; a
complete outcome never calls a sent record Skipped.
Optional provenance fields use provider_sent for local
RecordSent and target_saved for
protocol-confirmed settled admission, true exactly for
Saved.
store_stream
offers records one at a timestore_stream is the open counterpart of
store, as for_each is of list. It
creates an empty private provider Lace, opens an interlace with
store’s operands against the target, and drives it to the
first fixed point before returning; failure until then is a call
error.
Each offer(record) stores the record into the provider
and drives to the next fixed point. It returns Saved
when the provider emitted RecordSent for that record before
the fixed point, under store’s settled-admission rule, and
Skipped otherwise, at once and without a round when the
provider already held the record. A skipped record stays in the
provider: a later offer that makes the target select it sends it in a
later round, while its own offer remains Skipped. Offers run one at a
time.
An error, including an offer interrupted before its fixed point, ends
the stream: later offers return it, and records saved before it stay
saved. A peer that closes a connected target before an offer’s fixed
point leaves that offer unsettled; the stream ends with
PeerProtocol. close cancels the interlace and
closes the provider, which releases the offered records, and returns the
error that ended the stream with any cleanup failure under the close
rules above.
| Surface | Open, offer, close |
|---|---|
| Rust | store_stream(target),
store_stream_with_options(target, options);
offer(record) -> StoreRecordDisposition; consuming
close(), all async in Tokio |
| JavaScript | storeStream(target, options?) resolves a
StoreStream; offer(record) resolves
'saved' or 'skipped';
close() |
| Python | store_stream(target, *, limits=None);
offer(record) returns 'saved' or
'skipped'; close() and context exit |
for_each
observes without sending records backfor_each creates a persistent observer Lace. Its stream
contains at-most-once RecordSaved observations with the
observer-owned validated Record, plus operation-wide
FixedPoint and Closed. It exposes no
RecordSent, error events, callback queue, or record-only
subscription. Observer records do not flow back to the target.
Built-in constructors include:
Rust/native: Lace::filesystem(...), Lace::memory(...)
Rust: Lace::portable(...), Lace::memory_with_capacity(records, bytes)
Optional LMDB: Rust Lace::lmdb(...), Python Lace.lmdb(...)
JS/core: Lace.memory(...)
JS/browser: Lace.opfs(...), Lace.browserLocalStorage(...), Lace.openBestEffort(...)
JS/Node: Lace.filesystem(...)
Lace::memory_with_capacity(records, bytes) is a memory
Lace holding at most bytes of canonical record bytes,
counted once per record, so a duplicate holds nothing. It is a logical
quota: indexes and allocator overhead are not counted. Seeding beyond it
fails construction with LimitExceeded. An admission beyond
it fails that operation with LimitExceeded before anything
is admitted, so the store position does not advance and no open
operation wakes; other operations continue. Close releases the count; a
record a reader still holds stays readable on its own.
held_bytes() on such a Lace reports the canonical bytes it
holds now, and None on any other Lace. The JavaScript,
Python and C memory constructors are unbounded.
The non-clonable Lace exclusively adopts one concrete
store object. It owns mutation publication, retained
position-or-terminal-failure state, external-position polling
coordination, and lifecycle. LaceHandle duplicates none of
that state.
A store position is a measure of local admission progress, monotonic
within one adoption of a root, not a record identifier or public cursor.
At adoption, the runtime fixes immutable
position_may_advance_externally: false
guarantees position advances only through that Lace’s mutations;
true allows outside advances and enables fixed internal
polling. This is adoption metadata, not a store operation,
policy/protocol input, or operation option. Built-in constructors fix it
internally.
The native filesystem constructor adopts false and
exclusively owns its physical root until close or final owner drop.
Independent openers in the same or another process, including path
aliases, fail without waiting. Same-process operations share Lace
handles. Cooperating processes needing one shared root use optional
LMDB.
The LMDB constructor is present only with the consumer’s
lmdb feature. Enabling that feature for a target other than
Linux x86-64/AArch64 with glibc or musl, macOS x86-64/AArch64, Windows
x86-64/AArch64 MSVC, Android AArch64/x86-64, and iOS AArch64 devices and
AArch64/x86-64 simulators fails the build. Linux glibc and musl
processes may share one root. It adopts true. Each process
has only one independently opened environment per physical root,
including supported aliases. LMDB files are trusted local storage, not
an untrusted database-file import surface. Forked children do not use
inherited LMDB stores or records and exec before reopening.
Both native constructors take a root path; Python’s also accept the
operand_cache keyword below. A root’s capacity is fixed
when it is created: the platform default, or the byte count in
LACE_NATIVE_STORE_CREATE_CAPACITY. Reopening uses the
recorded capacity and ignores the variable. The native
capacity rule defines the defaults and valid values.
Forking with live native filesystem store or Lace objects is unsupported. Children MUST NOT use inherited store or Lace state. Fork before constructing these objects or exec before reopening; an existing owner’s filesystem root remains exclusive.
The portable constructor keeps records in plain files that also work
on a FAT card, such as an ESP-IDF device’s SD card. It takes a root path
and a format whose segment and index sizes apply only when the root is
created; reopening uses the recorded sizes. It adopts false
and exclusively owns its root until close or final owner drop: another
opener in the same process fails, and so does one in another process
where the platform has file locks. Records it returns stay readable
after close. It has no compaction. The portable file store
note defines its layout and recovery.
Python exposes:
lace.advanced.with_store(
store,
*,
position_may_advance_externally: bool,
) -> Lace
The keyword is required and must be an actual bool,
validated before reading the store’s initial position. Built-in
constructors pass their fixed values internally. Rust
lace-tokio and lace-rt expose no public
custom-store adoption surface. JavaScript exposes no generic
custom-store adoption surface or corresponding metadata argument.
While at least one relevant open operation waits at fixed point, a
Lace adopted with true performs finite authoritative
CurrentStorePosition polling. All such operations share
Lace-owned cadence and single-flight coordination: at most one physical
probe per fixed internal interval. Bounded operations and open
operations outside fixed point do not request polling.
An advance uses the same monotonic retained publication path as local mutation. An external probe or confirmed-position publication failure terminates the Lace as described below. Polling adds no public interval, notification source, protocol state, limit, or wire grammar.
Each Lace owns one operand cache, shared by its connected operations,
so that a later session receives a peer operand the Lace holds as its
hash only (050).
The setting counts entries, most recently used first, and defaults to
32; 0 holds none, so every peer operand crosses as a Blob.
The cache is memory only: a restart costs one Blob per peer operand.
list, get, store,
store_stream, and for_each over a connected
peer use a private Lace, whose cache starts empty.
| Surface | Setting |
|---|---|
Rust lace-rt / lace-tokio |
Lace::with_operand_cache(self, entries) -> Lace |
| JavaScript | operandCache in each Lace constructor’s
options |
| Python | operand_cache= keyword on each Lace
constructor |
Rust close consumes Lace: close().await in
lace-tokio, close() in lace-rt.
JavaScript awaits Lace.close(); Python calls
Lace.close(). Close:
Independent operation handles remain separately closeable but cannot
begin new store access. Retained Lace handles are invalid after close.
Repeated language-level close observes or waits for the same boundary.
Low-level C/WASM free() destroys allocations only; it is
not participant lifecycle.
An external-position probe or confirmed-position publication failure
retains the first terminal Store or
Integration error on the Lace. It rejects later claims and
store execution, terminates every subscribed operation with that same
error, and returns it from close after non-cancellable started work
drains. Ordinary operation-local store failures do not poison the
Lace.
Native close/drop does not publish posting lists. Filesystem close releases exclusive root ownership; LMDB close releases only that process’s environment. Where deterministic drop exists, resources remain alive while any handle, operation, or started work can access them; the final owner closes the concrete store. JavaScript requires explicit close for a deterministic boundary.
Native filesystem stores release their kernel lock on the persistent
lock rendezvous file explicitly before closing its
descriptor, never by deleting the file; a descriptor copy held by a
forked child that has not yet exec’d does not extend ownership. Node
removes its owned lifetime sentinel on clean close; browser Web Locks
release ownership without a lock file. Close and deterministic drop
neither probe PIDs nor remove possibly stale crash residue
automatically. Process/realm termination releases kernel locks and Web
Locks but may leave a Node sentinel.
Compaction shrinks a durable root in place. It is an offline call on a root with no open Lace; live operations never delete records.
Rust/lace-rt: compact_filesystem(root, policies) -> CompactionReport
compact_lmdb(root, policies) (feature `lmdb`)
Rust/lace-tokio: compact_filesystem(root, policies).await, compact_lmdb(...).await
Python: lace.compact_filesystem(root, policies), lace.compact_lmdb(root, policies)
JS/Node: Lace.compactFilesystem(path, policies, { wasm?, wasmUrl? })
JS/browser: Lace.compactOpfs(nameOrHandle, policies, { lockName?, wasm?, wasmUrl? })
Lace.compactBrowserLocalStorage(name, policies, { storage?, singleTab?, ...store limits, wasm?, wasmUrl? })
policies is a nonempty collection of the policy inputs
operations accept. A record is retained when at least one policy’s
stored-selection facet (SelectStoredRecord) selects it,
evaluated alone over the root’s complete local view at the root’s
high-water; every other record is removed. Exposure and directional
vetoes are ignored. A collection with no stored-selection rule, or a
policy whose stored selection depends on Here or
PeerConfidential, is a Policy error;
compaction MUST NOT evaluate absent session authority as false.
Selection is one pass over the unmodified root: a policy that relies on
evidence it does not itself select loses that evidence, and a later
compaction may remove more.
A compacted root restarts its StoreRecordId and
StorePosition numbering contiguously from the first
retained record, preserving relative admission order; position
monotonicity holds within one adoption. Removal is not retraction: a
peer may resend a removed record that an exchange policy still
requests.
The root must have no open Lace in any process. A native arena-backed
Record keeps its root’s arena in use after its Lace closes;
compaction fails with Store while any such record or Lace
of the root is alive anywhere, and with Store on any other
lock contention. While compaction runs, opening the root fails with
Store on every backend.
The call returns
CompactionReport { boundary, retained, removed, bytes_released, release },
where release is Ok or
Unsupported when the filesystem cannot release the freed
range after the root is already consistent. Errors are
Policy for retention-policy preparation,
LimitExceeded for a sweep evaluation bound, and
Store for IO, corruption, contention, or live records. A
compaction interrupted by failure, Tokio future drop, Python
interruption, or process termination MUST leave either the unchanged
root or a root that ordinary opening rejects and a compaction call for
that backend resumes. Resuming completes the interrupted selection and
does not evaluate the supplied policies. Interruption takes effect
before the next sweep store operation or move batch; dropping a Tokio
future is not a waitable boundary. Store compaction
defines the sweep and the physical algorithm.
Applications establish a byte channel; Lace carries ILTP over it. A
move-only, one-use ConnectedPeer atomically adopts either
one runtime-native duplex IO value or independent peer-to-local reader
and local-to-peer writer owners, alongside an explicit carrier role,
optional local By-secret, proof assurance, optional stream encodings,
optional port, and optional expected peer By.
Rust spells the constructors
ConnectedPeer::from_duplex(...) and
ConnectedPeer::from_split(...); bindings use idiomatic
factories. No intermediate public transport object is required.
Construction fixes role before decoding any ILTP byte:
CarrierRole::Listener is side0,
CarrierRole::Connector is side1. Paired endpoints use
complementary roles, never inferred or negotiated. Role is neither
participant position nor application identity or policy authority.
Construction defaults to Disabled proof assurance.
ConfidentialChannel asserts that the complete channel is
confidential, integrity-protected and endpoint-confined.
ChannelBound(binding) additionally binds live proofs to the
established protected channel. Each logical session begins with ILTP
0.13 prefaces, role-ordered optional Seal proofs and operands announced
by hash. Before policy evaluation the runtime freshly freezes
Here(local_key) and PeerConfidential(peer_key)
when present. Connected peer authority requires the live peer’s v3 proof
of the matching By-secret together with the supplied assurance; under
Disabled, a By-secret supplies Here and no
proof is sent. Direct operations carry neither fact. Endpoint
configuration and diagnostics supply no policy facts, plan/proof inputs,
or protocol authority.
ConfidentialChannel does not enable encryption or
authenticate the transport. Its assertion includes proxies, tunnel
endpoints, and plaintext forwarding legs. Raw unbound ILTP proofs are
fresh but not bound to the encryption session: an untrusted terminating
intermediary can relay live proofs between separate encrypted channels.
Anonymous encryption or TLS with peer verification disabled is
insufficient by itself. PeerConfidential relies on the host
excluding that intermediary, as defined by 050.
Applications own endpoint configuration/parsing, dialing, listening, acceptance, transport handshakes, admission, process creation/supervision, and task/service lifecycle. Lace supplies no shared endpoint enum, configuration, parser, dialer, listener, or process surface. Host admission counts and transport buffers are not interlace operation limits.
Lace implements no transport encryption. The application completes any protected channel establishment, including final handshake-output drain, before adoption. Its channel implementation owns setup deadlines, cancellation, cipher state and binding derivation. The binding is exactly 32 opaque bytes satisfying 050’s channel contract. It is not an endpoint, certificate fingerprint, remote identity or arbitrary shared nonce. The local implementation is trusted to supply protected IO and its binding; Lace signs and compares that binding without interpreting a transport method.
| Surface | Bound assurance input |
|---|---|
| Rust | ConnectedPeerOptions::with_peer_proof(PeerProofAssurance::ChannelBound(ChannelBinding::new(bytes))) |
| JavaScript | peerProof: 'channel-bound',
channelBinding: bytes |
| Python | peer_proof='channel-bound',
channel_binding=bytes |
| C/WASM | channel-bound assurance mode and 32 binding bytes |
JavaScript and Python also accept disabled and
confidential-channel; Python exposes those values through
PeerProofAssurance. They copy binding bytes during
construction. Missing/wrong-length binding, a binding supplied with
another mode, or bound assurance without a local By-secret is a
construction error before ILTP IO. Bound assurance requires a nonempty
valid peer proof even without a pin. Without a pin, that proof
authenticates the presented By, not an intended service or person.
Policy still determines permission.
The adopted writer’s flush completes once the channel implementation has accepted ownership of all preceding plaintext with bounded buffering. It then owns that output’s encryption, delivery or failure; flush does not mean the output reached the network. The final accepted batch’s original phase deadline covers flush before fixed-point publication. Shutdown MUST settle required owned work, including accepted output, and report its failure; transport failure MUST surface as IO error, not clean EOF. These are IO-provider obligations, not a promise that arbitrary byte-stream wrappers implement them.
Policy replacement retains the established channel and binding while repeating fresh ILTP proofs. Empty-store no-op runs no ILTP but may release an already established protected channel. Neither construction nor opening an interlace promises that the peer’s By has been verified. Standard browser WebSockets expose no TLS exporter; their URL does not supply a channel binding.
Connected construction accepts an optional rejection-only expectation:
| Surface | Input |
|---|---|
| Rust | ConnectedPeerOptions::with_expected_peer_by(ByValue) |
| JavaScript | expectedPeerBy, optional canonical
V.<b64a>.H3 text |
| Python | expected_peer_by, optional canonical
V.<b64a>.H3 text |
When an expectation is present, final resolved proof assurance MUST
be ConfidentialChannel or
ChannelBound(binding), explicitly supplied with the adopted
IO. The expectation never enables trust. Invalid text or an incompatible
assurance setting MUST fail the constructor through its existing
configuration-error path before protocol IO. Unbound verification does
not require a local By-secret; bound assurance requires one.
Each logical session checks the live, fully validated peer proof
against the expectation before advancing the received proof phase,
before operands or application-record work. A missing/empty proof or
valid wrong-key proof is PeerProtocol with explanatory
detail; malformed proofs retain normal validation errors. The
expectation supplies no fact, callback, generic required fact bag, plan
input, or authority, and does not change proof bytes or profiles.
Constructors and open-handle setup do not await proof. Failures
during driving use the ordinary typed failed bounded outcome or open
error channel, including retention on a failed open handle. Policy
replacement rechecks fresh proof against the same carrier-owned
expectation; changing it requires a new carrier. Direct pairs run no
proof and accept no such expectation. Empty store still
runs no interlace or proof. Neither direct operation nor empty-store
completion claims connected peer authentication.
An open connected interlace, and the outcome of a connected bounded
interlace, report the By the live peer proved in the latest
logical session: the key of that session’s PeerConfidential
fact (050).
It is absent before that proof validates, under Disabled
assurance, after an empty proof, from a re-preface until the fresh proof
validates, and on direct operations. It is a diagnostic, like the peer’s
port: never a policy fact or authority, and not an expectation. Where a
closed open handle remains readable, it still reports the value.
| Surface | Open interlace | Bounded outcome |
|---|---|---|
| Rust | OpenInterlace::peer_by() -> Option<ByValue> |
BoundedInterlaceOutcome::peer_by |
| JavaScript | peerBy, a string or null |
peerBy |
| Python | peer_by, a str or None |
BoundedInterlaceOutcome.peer_by |
| C/WASM | lace_interlace_machine_peer_by_ptr and
_len |
— |
Connected construction accepts one optional ordered list of 065 stream-encoding tokens:
| Surface | Input |
|---|---|
| Rust | ConnectedPeerOptions::with_stream_encodings(tokens),
ordered token names |
| JavaScript | streamEncodings, array of token strings |
| Python | stream_encodings, sequence of token strings |
The list is the carrier’s encode preference (Encode) and
its decode set (Decode). Omitting it offers every encoding
compiled into the build, zstd20c before
deflate12c. identity is always implied and
never listed, so an empty list offers identity only. An
unknown, uncompiled, repeated, or identity token MUST fail
the constructor through its existing configuration-error path before
protocol IO.
Each logical session’s preface pair selects each direction’s
encoding, so the two directions may differ; the list persists across
policy replacement. Observations, limits, and errors are unchanged, and
operation budgets count decoded bytes. Direct pairs and empty
store encode nothing. Compressed ILTP bytes gain nothing
from transport compression such as WebSocket
permessage-deflate; hosts that negotiate it can decline
it.
An open connected interlace, and the outcome of a connected bounded
interlace, report the token of each direction’s encoding that the latest
preface pair selected: outgoing for local-to-peer bytes, incoming for
peer-to-local bytes, identity included. Both are absent
before the first peer preface and on direct operations, and each peer
preface selects them again. They are diagnostics, like the peer’s port:
never policy facts or authority. Where a closed open handle remains
readable, it still reports them.
| Surface | Open interlace | Bounded outcome |
|---|---|---|
| Rust | OpenInterlace::outgoing_encoding(),
incoming_encoding(), each
-> Option<StreamEncoding> |
BoundedInterlaceOutcome::outgoing_encoding,
incoming_encoding |
| JavaScript | outgoingEncoding, incomingEncoding, each a
string or null |
outgoingEncoding, incomingEncoding |
| Python | outgoing_encoding, incoming_encoding, each
a str or None |
BoundedInterlaceOutcome.outgoing_encoding,
incoming_encoding |
| C/WASM | lace_interlace_machine_outgoing_encoding_ptr/_len,
lace_interlace_machine_incoming_encoding_ptr/_len |
— |
A compressed direction ends with an end mark. Bounded completion and
replacement write it; cancelling an open operation at fixed point writes
it only when the writer accepts it immediately, and terminal failure
never writes it. A peer that sees EOF without the end mark reports
framing truncation as PeerProtocol rather than
Closed(PeerClosed).
lace-sans-io compiles deflate12c with its
deflate feature and zstd20c with its
zstd feature, both off by default. lace-rt,
lace-tokio, and lace-ffi enable both by
default through same-named features, so the standard JavaScript and
Python builds include both. Constrained builds select features
themselves.
A connected peer may name what it wants to talk to over its carrier: a port, sent in every preface the carrier emits, with the text 060 defines. A port selects the receiving side’s policy, and in Rust may also select its local Lace on the carrier’s first session; it supplies no fact, identity, or authority (050). Without a port a carrier sends none.
| Surface | Outgoing port | Peer’s port |
|---|---|---|
| Rust | ConnectedPeerOptions::with_port(PortText) |
OpenInterlace::peer_port() -> Option<PortText> |
| JavaScript | port, a string |
peerPort, a string or null |
| Python | port=, a str |
peer_port, a str or None |
| C/WASM | port argument of connected-carrier construction | lace_interlace_machine_peer_port_ptr and
_len |
Invalid port text MUST fail the constructor through its existing configuration-error path before protocol IO. The peer’s port is the one its latest preface named, absent before the first preface and on direct operations; it is a diagnostic.
An open interlace of one connected peer and a local Lace may take a
selector in place of its policy. The carrier calls the selector once per
logical session, including after every re-preface, with the peer’s port
or none, before it announces an operand. The selector returns that
session’s policy with an optional expected peer By, or
refuses, which fails the operation with PeerProtocol
(port refused). The returned expectation is checked at the
session’s proof phase as one
supplied at construction is, under the same assurance requirement.
Supplying both a policy and a selector, a selector on another route, or
a connected peer constructed with an expected peer By is a
Policy call error. Policy replacement on the operation
returns Policy. A fixed-policy operation serves whatever
port its peer names.
In Rust the operation is opened with the peer alone, and the selector
returns the local Lace with the session’s policy. The Lace returned for
the carrier’s first session serves every later one, so a later call must
return it again; another Lace refuses the session like
None. Until the first selection the operation holds the
transport and its prefacing deadline but no Lace, so closing or failing
before it settles no store work; the selection installs the Lace’s
operand cache, record staging and position subscription, after which the
operation behaves as one opened on that Lace. Because its store is not
yet known, a selecting operation reserves its IO chunk buffers for a
staging store. JavaScript, Python and C open the operation on a given
Lace, and their selectors return the policy alone.
| Surface | Selector form |
|---|---|
| Rust | open_interlace_with_selector(peer, selector, options),
async in Tokio;
selector: FnMut(Option<&PortText>) -> Option<(LaceHandle, SessionOperand)>,
where
SessionOperand::new(policy).with_expected_peer_by(by) |
| JavaScript | openInterlace(lace, peer, { selector });
selector(port) returns or resolves to
{ policy, expectedPeerBy? } or null |
| Python | open_interlace(lace, peer, selector=selector);
selector(port) returns
SessionOperand(policy, expected_peer_by=None) or
None |
| C/WASM | null policy at connected-carrier construction; the
SelectOperand poll, answered by
lace_interlace_machine_select_operand |
A JavaScript or Python selector that throws or returns a malformed
value fails the operation with Integration, retaining the
thrown value as its cause; a returned policy that fails preparation
fails it with Policy. A returned expectation without
confidential-channel or channel-bound assurance fails it with
Integration.
Core, browser, and Node entries expose:
ConnectedPeer.fromSplit({ readable, writable, role, bySecret?, peerProof?, channelBinding?, streamEncodings?, port?, expectedPeerBy? })
ConnectedPeer.fromWebSocket({ socket, role, bySecret?, peerProof?, channelBinding?, streamEncodings?, port?, expectedPeerBy? })
fromSplit adopts unlocked
ReadableStream<Uint8Array> and
WritableStream<Uint8Array> owners. The source
supplies owned chunks with bounded buffering. The sink’s write promise
MUST settle only after the sink has accepted ownership of the chunk
within bounded buffering; its close MUST settle that owned output and
reject on its failure. Source cancellation and sink abort retire
physical IO. Pending writes and close operations MUST respond to the
writable controller’s abort signal, including rejecting active work on
IO failure. Lace cannot provide these guarantees for an arbitrary sink;
a platform stream conversion alone does not establish them.
fromWebSocket adopts an already-open standard WebSocket
with no pending output. It installs ingress handling before asynchronous
initialization; initialization failure retires the socket. Binary
messages carry consecutive ILTP bytes without semantic message
boundaries. Ingress is bounded; overflow is transport failure. Its
writes resolve when bufferedAmount reaches zero. Completion
initiates socket closure without waiting for peer acknowledgement;
failure or cancellation closes without draining. Platform-queued bytes
may still arrive. Neither the URL nor successful adoption supplies proof
assurance; dialing, TLS, and admission remain application-owned.
Connected inputs are consuming arguments to bounded/open interlace,
list, get, store,
store_stream, and for_each. Responsibility for
the input precedes prepared participant claiming into the runtime
driver. Neither accepting an input nor preparing policy starts protocol
driving. Every accepted resource MUST have exactly one owner through
preparation, construction, execution, and teardown.
| Runtime | Input acceptance |
|---|---|
| Synchronous Rust | By-value arguments move into the call before its preparation; failure and no-op paths release them through ordinary RAII. |
| Tokio Rust | By-value arguments move into the returned future at creation; its body and protocol driving run only when polled. Dropping an unpolled future releases its inputs without asynchronous graceful shutdown. |
| JavaScript | At function-body entry, before fallible options, policy, record or route preparation and before the first await. |
| Python synchronous runtime | At function-body entry, after language argument binding and before fallible preparation or user-defined record iteration. |
JavaScript and Python MUST take each distinct available genuine
ConnectedPeer resource from the supplied participant
positions before other setup validation. The public wrapper then owns no
transport. Repeated aliases, already-consumed inputs, and
two-connected-peer routes are invalid. A failed invocation MUST release
every resource it took exactly once and MUST NOT access resources owned
by an incumbent operation. An unavailable argument does not authorize
cleanup of its incumbent owner. A failed transport still owned by an
available peer is taken before its failure is reported, rather than
abandoned during validation. Arbitrary objects with ownership-shaped
methods are not connected inputs. Neither is a LocalPeer:
it is never taken or consumed and remains reusable.
After transfer, JavaScript/Python peer close() and
Python peer context exit are harmless: they neither close nor await the
operation that received the resource. An unsubmitted peer still requires
its ordinary close. There is no public claim-state query, disposal
receipt, or post-transfer IO backdoor. Language argument evaluation or
binding may fail before the operation is entered; such failures are
outside its acceptance boundary. Local Lace participants remain
caller-owned and are retained through their handles. Store-adoption
validation order is unchanged.
Preparation or partial construction failure MUST attempt cleanup of
every acquired resource, including private participants, codecs, and
foreign allocations not yet published in a handle, and finish required
owned work. JavaScript awaits failed-setup cleanup before rejecting;
Python finishes synchronous cleanup before raising, including for
BaseException interruption. Successful open setup publishes
the owning handle only after construction succeeds. It does not imply
that peer proof or convergence ran. Rust RAII release remains distinct
from explicit waitable close. No runtime may use failed setup to submit
work to an incumbent or retired machine.
After acceptance, callers cancel the operation rather than close its IO directly. Cancellation retires the machine and initiates transport closure immediately, then waits for started store work’s durable classification and position publication before reporting the waitable cancellation boundary. The same ownership covers started authoritative refreshes. That work MUST NOT submit to a retired machine. An interrupted wait MUST NOT release resources still needed by mandatory work; the runtime retains and drains that work before completing its waitable boundary. Cancellation is not a protocol fact, bounded terminal, or resource limit.
Tokio also exposes an owning
BoundedInterlace::new(first, second, policy, options),
which bounded_interlace uses. Its borrowing
run(&mut self) returns the ordinary bounded outcome
after required output and route settlement, and the same outcome on
later calls. Dropping that future leaves the operation owned, its
observations retained and closeable; a later run resumes
it, except that interrupted route settlement cannot resume and fails
with Integration. Consuming
close(self) -> Result<()> cancels the operation,
retires transport, and settles started work through durable
classification and position publication. It MUST preserve existing
operation and independent cleanup failures under the close rules above.
Successful close reports settled local cancellation, not convergence; it
returns no bounded terminal or observations. Dropping the owner or its
close future is a non-waitable fallback.
Dropping a borrowing Tokio event-read future does not drop its owning open operation; dropping a Tokio task handle does not cancel its task. Ignoring a JavaScript promise or collecting a Python object is not waitable cleanup. Python store workers execute owned StoreWork while the machine and position publication remain with the thread driving the operation. These rules add no asyncio API or finalizer.
A carrier can carry successive logical sessions, each with an immutable policy and plan. Ordinary open interlace exposes synchronous hard replacement through the same exclusive mutable handle:
Rust/lace-tokio and Rust/lace-rt: open.replace_policy(next_policy)
JavaScript: open.replacePolicy(nextPolicy)
Python: open.replace_policy(next_policy)
The argument uses the same policy input as opening the interlace. Preparation failure leaves the interlace unchanged. Success means an equal operand was a no-op, the forming prefacing operand was replaced, or the latest replacement was staged for a running owner. Staging performs no polling, stepping, session retirement, store/transport IO, writer-batch mutation, or event consumption/production. It does not wait for activation. Later calls may supersede the staged operand before the next drive step.
For an existing running carrier, at the next poll or step before
endpoint work or retained Lace-state delivery, a connected owner retires
the complete logical session and re-prefaces on the same carrier. Fresh
proof, role-ordered operand announcement, authority, plan, and rounds
follow. A peer re-preface makes the same hard cut. Direct replacement
stages both prepared operands, the next operand for each bare position
and any LocalPeer operand unchanged, and on the next step
after both endpoint store effects settle, atomically recreates the pair
with unchanged explicit authorities and limits. It does not replace one
slot independently.
Replacement cannot run concurrently with event driving, store
execution, or another operation on the handle. JavaScript and Python
reject it while asynchronous drive/store work owns the handle. After
clean closure or cancellation it returns Policy (the
interlace is not open); a failed handle returns its retained typed
error. A selector-form operation returns Policy: its
selector supplies each session’s policy. Porcelain, capture, provider,
and observer wrappers keep fixed operation-owned policy and expose no
replacement.
Each fixed-point entry has a fresh opaque local identity to reject
stale store-watch readiness. Replacement creates no
PolicyReplaced event, activation waiter, or
policy-identifier event; ordinary lifecycle observations do not identify
which staged operand activated. Host-accepted observations remain
deliverable history; observations still owned by the retired machine are
dropped.
Lace-130 defines
self-contained Interlang compilation and structural composition of
complete canonical policies. A source convenience accepts one Lace-120
contribution with local friendly helpers, effects, and
datalog: blocks. Applications supply complete source bytes;
compilation owns no IO, loader, resolver, or evaluator. Source has no
module wrapper, imports, or external relation interface.
Compilation validates all authored rules and dependencies before execution pruning, including unused rules. Each prepared policy MUST have at least one standard 040 facet rule. Empty and helper-only sources are preparation errors; veto-only policies are valid. A side-local executable still requires two ordered operands, their plan, and a typed local slot.
Independent prepared policies combine through 130’s n-ary constraint composer. It isolates private helpers, intersects contributed selection/exposure facets, and unions directional vetoes. An absent facet adds no constraint on that facet; a defined facet with no rows constrains its result to empty. An optional guest collection constrains the result likewise while its rules see only records the authoring inputs expose, and optional field exclusions remove matching records from every composed facet and from that view, as 130 defines. Every input MUST be facet-bearing. Input order and duplicate identical canonical inputs do not change one call’s canonical output. Bindings MUST pass all complete collections to one composition call, not fold a binary operation. Runtime-owned porcelain constraints use the same composer and add no policy mode or third exchange input.
Rust exposes compile, compile_with_limits,
and compiler-independent prepare_facet_constraints;
JavaScript exposes compile and
composePolicies; Python exposes compile and
compose_policies. The exact preparation contracts and
limits are defined in 130. Helper outputs for generic finite evaluation
use the separate canonical 030 Datalog surface, not an Interlang
compilation target.
Execution APIs MUST also accept prepared canonical operands without an authoring frontend. Raw canonical input receives ordinary 030/040 validation. Parsed rules may be retained alongside canonical bytes rather than reparsed for unchanged local results. Bindings project one Rust compiler, composer, validator, and evaluator; they MUST NOT implement language-local parsers or composers.
All preparation finishes before prepared participant claiming into a
driver or replacement staging. Input acceptance under the connected
ownership rules above occurs earlier and is not protocol activation.
Failed opening releases accepted inputs; failed replacement leaves its
existing operation unchanged. Applying a prepared operand follows hard
replacement above. Runtime preparation failures use Policy;
source-byte and fixed preparation-bound failures use
LimitExceeded. Application loading and lookup failures
remain with the application. Preparation results are neither bounded
outcomes nor observations.
Tools may explain selection, exposure, route choice, and failure through source maps, rule identifiers, traces, and route reports. Diagnostics are neither normal application events nor protocol facts and do not alter canonical policy text.