Contents

Lace Runtime Composition and Ownership

This note defines the cross-runtime composition below Lace-100: who owns a participant, operation, store, transport, and public projection. The numbered specifications define observable semantics. Mechanism-specific architecture notes own state transitions, policy compilation, store queries, durable layouts, wakes, and foreign store-result construction.

Composition boundary

The implementation has four ownership layers:

  1. lace-sans-io owns records, canonical Datalog validation/evaluation, fixed exchange lowering, ILTP codecs including 065 stream encodings, and synchronous interlace machines.
  2. A concrete runtime owns Lace, LaceHandle, operation drivers, asynchronous store and transport execution, waits, cancellation, deadlines, and public value projection.
  3. One concrete store object adopted by one Lace owns policy-neutral persistence, search, load, and store access. Each physical root owns its record identities, admission order, and authoritative committed state.
  4. The application owns listener binding, connection admission, service task supervision, reload, shutdown, and process lifecycle.

lace-compiler compiles one complete self-contained Lace-120/130 source into an ordinary facet-bearing policy and depends on the core, never the reverse. Runtime source conveniences call it; precompiled execution, including porcelain and policy replacement, does not require it. Applications own file/code loading; the compiler owns no IO, resolver, evaluator, or persistent compilation lifecycle. The core’s n-ary canonical constraint composer also serves precompiled porcelain and is available without the compiler feature. It rejects an empty authoring collection and each helper-only input, isolates helpers, intersects contributed selection and exposure facets, unions vetoes, confines guest inputs to the authoring exposure view, and removes records matching field exclusions.

Compiler availability is a build boundary, not a policy mode:

Stream-encoding codecs are likewise a build boundary, with the features Lace-100 defines; the JavaScript WASM build and PyO3 receive them through lace-ffi. The compiled-in set fixes the default connected offers and the tokens connected construction accepts.

Core interlace and Datalog machines own no store, socket, task, timer, callback, worker, public event queue, or language runtime. The lace-sans-io::runtime_support module may provide synchronous policy-neutral stores, store-building tools, and codecs, including native filesystem IO and the IltpCodecState shared by lace-rt and Tokio byte-stream transports. Concrete runtimes own asynchronous execution and communication between those parts.

The optional lmdb-lace workspace crate implements the existing synchronous store operation boundary and depends on lace-sans-io. lace-rt, Tokio, FFI, and PyO3 enable it with their default-off lmdb feature; each retains its direct sans-IO dependency. There is no reverse dependency, public low-level LMDB API, or new store protocol. Its physical profile is owned by native-lmdb-store.md.

A runtime drives typed machine effects and inputs. It does not mirror semantic phase, reinterpret ILTP item families, duplicate policy selection, or maintain a second state graph.

Encryption and transport establishment belong to applications. Lace adopts established byte IO with Disabled, ConfidentialChannel, or ChannelBound(ChannelBinding) assurance; it implements no encryption suite or handshake. Generic v3 proof validation signs and compares the immutable opaque 32-byte binding. Its derivation and protected-IO semantics belong to the channel implementation under Lace-050/060; ordinary Seal construction is unchanged.

Participant ownership

A Lace is one non-clonable runtime participant owner. It exclusively adopts one concrete store object together with one immutable position_may_advance_externally boolean and owns:

false guarantees position advances only through mutations executed by that Lace. true means an outside advance is possible; it enables fixed internal polling and leaves CurrentStorePosition as the sole position authority. The boolean is adoption metadata, not a StoreOp, runtime fact, protocol input, or operation option. Concrete constructor assignments are owned by store-record-fact-access.md; Lace-100 owns the public custom-store surface.

A native filesystem Lace exclusively owns its root; an independent opener fails on lifetime-lock contention. LMDB permits cooperating processes to open one root, with one environment per root per process. Same-process operations share Lace handles. Lace construction publishes its initial position only after authoritative CurrentStorePosition succeeds; an unknown or malformed initial result fails construction as Store rather than inventing an empty position, and initial publication failure fails construction as Integration. No participant is exposed after either failure.

Optional Rust Lace::lmdb(path) and Python Lace.lmdb(path) constructors establish the initial authoritative position and adopt with external advancement enabled. LMDB permits only one independent environment per root per process; same-process callers share Lace handles. LMDB child processes exec before reopening and do not use or destroy inherited C state.

Forking with live native filesystem store or Lace objects is unsupported. Applications fork before constructing these objects or exec before reopening; children do not use inherited owners.

A LaceHandle is a clonable opaque capability for that Lace. It duplicates no store, lifecycle, position source, socket, transaction, or protocol machine. Every operation retains one handle for each local participant position. Two handles may refer to the same Lace in direct composition.

A LocalPeer is a clonable local participant: one LaceHandle plus one prepared policy operand. It owns no IO or claim state and is never taken as a connected input. Route construction replaces its position’s operand with its own before claiming, and an open direct operation retains that operand for replacement.

A ConnectedPeer is a separate move-only one-use capability that atomically adopts host-established IO and explicit carrier role, optional By-secret, proof assurance, stream encodings, optional port, and optional expected peer By. Bound assurance requires a local secret and an owned channel binding. There is no required public intermediate transport object. Connected input acceptance transfers that resource into the invoking call or future before prepared driver construction. A bounded operation settles it; an open operation retains it until explicit close, clean peer closure, failure, or drop. It is not a Lace and does not own a local store.

Rust’s by-value call/future ownership already precedes internal participant claiming. An unpolled Tokio future owns its moved inputs but has driven no protocol; drop releases them without async graceful shutdown. JavaScript and synchronous Python take genuine connected inputs at body entry, before fallible preparation or user-extensible input processing. Python argument binding happens first; JavaScript takes resources before its first await. Lace-100 defines the exact acceptance and invalid-input cases. None of these boundaries moves protocol activation or policy replacement staging ahead of preparation.

Dynamic peer transfer removes the owned payload from the public wrapper. The invocation retains it through private participant allocation, policy preparation, and driver construction, then either publishes the owner or disposes the payload. The driver owns the transport directly, not a source wrapper with privileged cleanup access. An emptied wrapper’s close is harmless and cannot close or wait for its recipient. Distinct available inputs are taken once even on invalid routes; rejected reuse never touches an incumbent operation. Private branding identifies ownership, not arbitrary getters or duck-typed close hooks. Existing payloads and setup locals suffice; no lifecycle registry or additional ownership layer is needed.

InterlaceSide, CarrierRole, and ParticipantPosition remain distinct:

Runtime drivers perform the mapping. Protocol side and carrier role are not public participant identity or authority.

Lace close and final drop

Explicit Lace close is the final waitable participant boundary. It:

  1. marks the owner closing so retained handles cannot begin new store access;
  2. requests closure of operations directly registered with runtimes that provide that relationship;
  3. waits for every already-started store read, mutation, or authoritative position refresh to finish, including durable classification and confirmed position publication;
  4. releases the adopted store object’s resources; and
  5. returns the first retained terminal failure, if any, preserving distinct cleanup failures through the binding’s existing error channel.

An external-position probe failure (Store) or confirmed-position publication failure (Integration) atomically poisons the Lace. Poison wakes every subscriber with the same retained error and rejects every later store execution or participant claim. Close still drains already-started non-cancellable work before returning that first error. Ordinary operation-local store failures do not poison the Lace.

Native close and drop perform no posting-list checkpoint. Filesystem close releases its lifetime kernel lock after active work settles. LMDB close releases only that process’s environment; other processes may continue using the root.

Rust consumes Lace (async in Tokio and synchronous in lace-rt). JavaScript and Python expose repeatable language-level close calls that wait for or observe the same completed boundary. Python strongly retains each directly registered open operation until the operation unregisters or Lace close attempts every registered close; that explicit boundary clears the registrations and breaks the ownership cycles before releasing the store. Independently owned operation handles remain separately closeable and cannot start new access after their Lace closes.

In runtimes with deterministic destruction, final owner drop is the non-waitable resource-safety fallback. Handles, operations, and already-started work retain the concrete participant resources they can still access. The native filesystem lifetime lock remains held until those owners release the store. JavaScript requires explicit Lace close for a deterministic boundary; process or realm termination releases Web Locks but may leave a Node sentinel. Low-level C/WASM free functions destroy allocations only; they are not participant close.

Operation ownership and close

Once transport establishment completes, one host operation owns exactly one semantic InterlaceMachine plus its runtime-specific execution state: participant handles, pending store work, transport and codec ownership when connected, deadlines, cancellation, waits, and bounded public queues. Tokio, lace-rt, JavaScript, and Python implement those drivers independently; there is no shared runtime context, scheduler, handle table, or generic asynchronous executor.

A direct machine owns two endpoint machines and routes typed peer output between them. A connected machine owns one carrier/session composition and uses the runtime-owned byte transport. These are variants of one semantic operation owner, not two application APIs.

An open operation has one exclusive mutable owner for event driving and policy replacement. One-event reading is canonical; iteration and run-to-boundary helpers consume that reader rather than starting another driver. The handle retains terminal failure independently of one-shot error delivery.

Open and observer handles are explicitly closeable. Rust uses consuming close(self) (async in Tokio); JavaScript and Python use their ordinary explicit close forms. After a connected peer is claimed, callers signal runtime-local operation cancellation rather than closing its IO directly. Close cancels the semantic machine, prevents further driving or replacement, waits for already-started store and authoritative-refresh work through durable position publication, and settles connected transport ownership. Retirement of an operation that started an external probe does not cancel publication or Lace-wide poisoning. Rust returns the final local Closed(Cancelled) boundary appropriate to the handle when no terminal failure wins. Drop performs the same retirement without a waitable result. Dropping a borrowing Tokio event-read future does not drop its operation; dropping a task handle does not cancel its task. JavaScript promises and Python garbage collection supply no equivalent waitable destruction boundary.

Setup protects resources as they are acquired, including foreign error/output allocations, codecs, private participants, and transferred transport. If a later construction step fails, cleanup releases already-acquired resources even when no driver was returned. Python covers BaseException interruption as well as ordinary exceptions. Its store workers receive owned StoreWork, never the machine or transport ownership. An interruption during mandatory draining is deferred through that boundary, not treated as proof that the worker stopped. Retired work retains its Lace through durable classification, refresh, and publication, without machine submission.

JavaScript open setup is async so construction failure can await cleanup before rejecting. Success returns a constructed handle, not proof establishment or fixed point. Observer setup uses that same settled constructor rather than collecting detached setup-cleanup promises. Synchronous Rust/Python and Tokio retain their natural synchronous and future-based opening forms respectively. No asyncio surface, eager wrapper, task or finalizer is added for API symmetry.

Explicit close attempts every owned cleanup action and waits for required work, including active observer event projection before capture release. It retains primary and distinct cleanup failures through the existing exception/error channel. JavaScript uses AggregateError for combined failures; typed projections retain its causes, not just its message. Replaying a retained failure does not add it again. Repeatable close retains one pending/settled result, not a second cleanup status. After driving starts, bounded collectors preserve partial results and expose cleanup failure as a structured failed terminal; setup, empty-store disposal, and open failures use their call error channel. Cancellation presence is separate from its reason value, including falsy JavaScript values. Canonical event consumption gates both existing waiters and new reads on required cancellation settlement, so queued success or exhaustion cannot overtake it. No cleanup result certifies remote acknowledgment or physical release by an arbitrary source/sink.

A bounded operation owns teardown internally and returns only its outcome; Tokio’s owning BoundedInterlace also offers consuming close, so a caller that stops driving can wait for settlement. It reaches complete only after semantic fixed point and required final output and transport settlement. Participant capabilities are not returned with outcomes; callers retain reusable local access by cloning a LaceHandle before starting the operation. Empty store still releases its accepted connected input without creating an interlace or producing observations. Public porcelain entry points pass accepted resources to private runners, not another consuming public entry point. Argument-shape conveniences are separate binding choices; JavaScript array/promise shapes do not constrain Rust or Python collections.

Detailed carrier/session transitions, endpoint effects, deadlines, backpressure, EOF, replacement cuts, and raw transport completion are owned by interlace-transition-tables.md.

Direct and connected composition

Direct composition receives two local Lace handles, two prepared policy operands (the operation’s operand for each bare handle position, a LocalPeer’s own operand for its position), explicit final SessionAuthority values, and one limits value. It constructs a DirectPair and starts both endpoint machines without sockets or ILTP byte encoding. Typed peer output still crosses the same semantic machine boundary; direct composition never copies records between stores or bypasses store operations.

The two handles may refer to different Laces or to one Lace. Same-Lace composition uses one mutation/refresh sequence, one external-poll cadence when enabled, and one retained position-or-terminal-failure fanout. It adds no protocol-visible store identity or special interlace path.

Connected composition receives one claimed connected participant and either one local Lace handle with one operand, which takes the participant’s expected peer By, or, in the Rust runtimes, a selector alone, which supplies each logical session’s operand and expected peer By, requires a participant without one, and with the first session’s operand supplies the local handle. The connected participant fixes carrier role, proof assurance and any binding, optional expected peer By, the local By-secret input, stream encodings, port, and a fresh nonce seed. The driver answers each SelectOperand poll from that selector; the core owns no callback. Until the first answer a selecting driver owns the transport, its codec reserved for a staging store, and the prefacing deadline, but no handle, subscription, staging, or store work; the first answer installs the handle, hands its operand cache to the carrier, sets the codec’s staging, and subscribes to its retained state, and a later answer naming another handle is refused. Foreign hosts open on a given handle and select operands only. Endpoint configuration and diagnostics remain outside policy facts, plan and proof inputs, and protocol authority. Rust owns prefacing, proof and operand establishment, plan/session state, semantic validation, and per-direction stream-encoding selection and state; the runtime owns byte movement, codec driving, IO, and transport settlement. The codec produces each accepted batch in chunks of at most its configured chunk size and decodes compressed peer bytes into a buffer of that size. It retains control items but not records: a record item passes through the incremental record validator, and the codec hands out its prefix, then its data pieces, then its verified header. The codec’s owner writes each piece to its record sink before decoding continues. Before each input turn the runtime passes the carrier’s peer_record_ingress to the codec state, which checks each record at its start, before it reaches memory or a sink, and routes it as the transition tables define: establishment records, proofs and requested operand Blobs, go to memory whatever the store; an outstanding response goes to the store’s record sink; a record during reset prefacing is discarded, its length handed to the carrier as a PeerInput. A store with staging (the native filesystem and portable file stores) supplies a private staging directory through its StoreBackend; a connected operation receiving into it writes each record to its own staging file and syncs it before it becomes a file-range record, which the store copies at admission in bounded pieces. Other stores receive into memory: one allocation of the record’s canonical length, which the record then owns. Only the verified header turns a sink into a record, so nothing unverified reaches the carrier, facts, store or observations; a failed or partial record is dropped with its sink, which removes its staging file. A file-range record being sent is read in pieces of at most one chunk. The codec state hands staging writes, syncs and these reads to its owner as one blocking job at a time and neither decodes nor produces output until the job returns: lace-rt runs it on the driving thread and Tokio on its blocking pool, keeping the task across interrupted waits. Ingress reads nothing more meanwhile. A staging or read failure is a Store failure of the operation. A direct pair has no codec and encodes nothing. A record stream reader receives each record through the same validator and record sink, without ILTP framing, running its staging jobs the same way; its record-start check is the stream’s remaining byte bound.

The runtime owns these buffers’ size and budget. The codec state allocates each chunk buffer on first need: one retained read buffer the transport reads into directly, a compressed direction’s decoded input, the output chunk, a file-range record’s read piece, and with staging the piece a staging write holds (or a record’s metadata prefix). IltpCodecState::chunk_buffer_bytes bounds them. Each Rust runtime owns one process-wide io_budget::IoBudget and creates the codec with its configured chunk size when a connected operation is set up, reserving that bound from the budget. Releasing the transport’s IO, after completion, closure, failure or cancellation, drops the codec and the reservation. lace-rt runs blocking jobs on the driving thread, so none outlives its transport; a Tokio blocking job holds a share of the reservation, so a released carrier’s buffers return once its job settles. A Lace passes the chunk size to its store at adoption through StoreBackend::adopt and, when the store has staging, reserves the store’s copy buffer until the store’s close has run.

Direct composition accepts explicit final authority because both endpoints are inside one trusted composition. Connected authority is freshly derived from a live protocol proof together with a host confidential-channel assertion or a binding supplied with locally established protected IO; a host cannot inject final peer authority. ChannelBinding::new([u8; 32]) owns the opaque binding bytes for that channel. Lace trusts the channel implementation’s derivation and IO, but verifies the bound proof itself rather than accepting a provider-returned peer By. Policy operands and executable provenance follow the ownership contract in interlace-policy-lowering.md.

A fixed-operand carrier retains its immutable expected peer By and supplies it to every fresh session; a selecting carrier supplies each session’s selected one. Rust establishment compares the validated peer proof before advancing its proof phase; foreign hosts do not inspect proofs or perform a parallel check. Lace-100 owns public inputs and outcomes, Lace-050 owns proof validation, and Lace-060 owns the wire grammar.

Both compositions expose the same public events, errors, bounded outcomes, open boundaries, and porcelain semantics.

Store execution cut

The store is a policy-neutral StoreOp -> StoreResult | StoreError boundary. It owns admitted bytes and coherent search, load, and store execution. A native physical root owns its root-wide StoreRecordId/StorePosition; each operation observes that authority through its backend’s coherent read boundary. The store does not receive policies, exposure viewers, Datalog joins, protocol sides, public collectors, open-operation registrations, or wake callbacks.

An endpoint machine owns at most one active StoreCall. The runtime executes that request against the Lace selected for the endpoint. The Lace owns a mutation through durable classification, mandatory current-position refresh, and position publication before successful machine submission. The runtime submits every completed response, including a StoreError, to the machine, which owns the resulting terminal; only a failed mandatory refresh or confirmed-position publication fails the operation in the runtime. Every other same-Lace store call waits outside the mutation-through-publication interval.

When the adoption boolean is true, fixed-point drivers consult the Lace-owned poll state. The first eligible driver starts one Lace-owned CurrentStorePosition refresh; other operations wait on retained Lace state or the shared next time rather than queueing or joining the probe. Polling occurs only while a relevant open operation is fixed, never for bounded operations or active rounds. The refresh is runtime work, not a machine StoreCall, and uses the same publication gate and retained signal as mutation. Failure of the external probe or any confirmed-position publication poisons the Lace and wakes all subscribers. A terminal machine retires its call, but already-started non-cancellable store or refresh work may retain the Lace long enough to classify durable progress. It never submits to the retired machine, and close returns the retained first terminal failure after draining that work.

Compaction composes the sans-IO RetentionSweep machine with one concrete store outside any Lace. The compaction call opens the root’s store under the root’s rendezvous lock, executes each sweep StoreCall through that store’s ordinary operation execution (on the calling thread in lace-rt and Python, which releases the GIL, on a blocking thread in Tokio, and asynchronously in JavaScript), and then passes only the kept set to the store’s physical compaction entry. No Lace, position publication, polling, or interlace operation exists during the call, and the store receives no policy. Sweep, files, locks, and recovery are owned by store-compaction.md.

This paragraph is the cross-runtime ownership rule, not the response-building contract. Semantic operations, query/fact projection, coherent reads, and store trust are owned by store-record-fact-access.md. Move-only StoreWork, result construction, reserve/commit, ticket validation, and late foreign completion are owned by store-request-response-ffi-boundary.md. Open fixed-point registration and wake races are owned by open-store-wakes.md. Filesystem commit, recovery, lock, and index mechanics are owned by filesystem-shaped-store.md.

Host-established transports and listeners

A runtime atomically adopts host-established ILTP IO into a one-use ConnectedPeer. Applications own endpoint configuration and parsing, dialing, listening, and connection acceptance. There is no shared endpoint enum, configuration, parser, dialer, or listener API. Construction fixes CarrierRole::Listener or CarrierRole::Connector before decoding. Paired endpoints use complementary roles; IO shape and accepted or dialed provenance do not infer role. The capability owns no Lace, policy, listener, process, admission limit, hidden background task, or service lifecycle.

from_duplex / from_split adopt either one runtime-native duplex IO owner or independent peer-to-local reader and local-to-peer writer owners. They default to Disabled proof assurance. Process stdin/stdout and child pipes use that same split composition; applications explicitly transfer their IO owners. ConfidentialChannel is the host’s assertion about the complete byte channel, not transport protection supplied by Lace. The full-route assurance requirement and the distinction between proof freshness and channel binding are owned by Lace-050. Endpoint configuration and diagnostics are not policy facts, plan or proof inputs, or protocol authority.

The accepting call or future owns connected IO through prepared construction and operation settlement. Bounded completion settles final output, a compressed direction’s end mark, and shutdown as the transition tables define, then releases raw IO; an open operation retains the IO at fixed point. Runtime-local cancellation, failure, clean peer closure, and drop retire it through the connected lifecycle. Callers do not retain a post-claim IO backdoor. Applications continuously drive listeners, open or adopt process IO, accept and admit connections, spawn and supervise interlace operations, and stop and join them. A service may retain one Lace while replacing listener tasks. Raw transport constructors do not own a generic Lace server, process supervisor, admission policy, reconnect loop, or application shutdown graph.

Bound IO adoption and flush

Applications complete protected-channel establishment before connected adoption. They own handshake deadlines and cancellation; no pending encrypted-establishment state exists in the runtime driver. The prepared operand constructs an ordinary ILTP carrier using the supplied immutable binding and secret. Synchronous policy replacement changes only ILTP state, preserving the established channel.

An IO adapter’s flush completes when the channel implementation has accepted ownership of all preceding plaintext within bounded buffering; it then owns any encryption and delivery. The ordinary retained writer preserves the final accepted batch’s phase deadline and withholds fixed-point publication until that flush settles. The adapter supplies bounded authenticated input and independent read/write/flush progress. Transport protection errors become IO failures, not clean EOF. Shutdown settles owned output and reports its failure; failure or cancellation aborts IO. Lace-050 owns these IO obligations and the transition table owns the driver cut.

Empty-store disposal releases adopted IO without ILTP or an authentication claim; it does not claim the application performed no earlier transport handshake.

JavaScript byte IO

Core, browser, and Node entries share ConnectedPeer.fromSplit over standard Web Streams and ConnectedPeer.fromWebSocket over an already-open standard WebSocket. Both use one byte-stream transport and the existing connected carrier driver; there is no Node-specific connected driver or public transport object. The Node entry adds filesystem adoption only.

The byte-stream transport locks one reader and writer, requests input only when the driver needs it, retains one bounded chunk without read-ahead while it is in use, and retains one write until its sink accepts ownership. The driver keeps requesting legal peer input while the writer owns a batch, so two endpoints writing at once never wait on each other’s reads. Source/sink implementations own physical IO cancellation, bounded buffering, delivery of accepted output, and immediate rejection of active work on errors. Generic stream conversions do not necessarily provide bounded or interruptible-write semantics.

The WebSocket adapter supplies those streams. It installs listeners synchronously on adoption, accepts binary messages, and uses a bounded byte queue. Since standard WebSockets expose no drain event, a fixed internal timer checks bufferedAmount only while output is pending, bounding the platform buffer to one write. The write promise resolves at zero; it is not a peer acknowledgment. Close after successful drain initiates the socket handshake and releases local ownership; abort cancels local pending work and initiates closure without draining. Already platform-owned bytes cannot be recalled. Timers and listeners are retired with the adapter, and a socket cannot be adopted twice.

Shared binding and FFI layering

The safe Rust binding layer projects typed machine values for foreign hosts. The C/raw-WASM ABI is a thin ownership and value projection over that layer; PyO3 wraps the same safe Rust types directly instead of calling the C ABI. Native Rust hosts use lace-sans-io directly.

The binding layer exposes opaque interlace, private one-item codec, record, policy, and move-only store-work parts needed by runtime implementors. It owns no Lace, store adoption, scheduler, worker, future, socket, route map, current-position watch, public event buffer, or application lifecycle. Every owning foreign pointer destroys only its Rust-owned allocation.

The C/WASM ABI version is defined in ABI.md; the Python extension version gate is in python/lace/_native.py. Each host checks the exact version for its surface before other calls or type use. There is no alternate-version projection. lace-ffi owns the C/WASM declarations and shared safe-Rust inventory. python/crates/lace-py owns the PyO3 module inventory and python/lace/_lace_py.pyi.

Policy composition borrows the complete authoring and guest collections of prepared-policy owners and the field exclusions, and calls the core n-ary composer once. The C/WASM boundary accepts one array of policy pointers and its length per class and one array of borrowed exclusion texts, not two policy operands to fold repeatedly. JavaScript and Python use the same collection semantics. Inputs remain caller-owned; success returns an independently owned policy. Source compilation takes complete UTF-8 text and a source-byte bound and returns a prepared policy directly.

Core canonical preparation and module construction preserve the existing PreparationError through shared LacePolicy and PyO3 APIs. Canonical C/WASM construction and composition project malformed or invalid policies as Policy, and fixed rule/arity/static TopK bound failures as LimitExceeded, including without the authoring compiler. Interlace and ILTP entry points likewise carry the core’s typed category for construction, replacement, and machine failures (an oversized local operand is LimitExceeded on both routes) and PeerProtocol for decoder failures. Foreign hosts do not recover categories from strings. This is local preparation: an invalid received operand remains PeerProtocol at the carrier’s peer-input boundary.

Foreign hosts own side-to-Lace routing, asynchronous store and external-position refresh execution, durable position-or-terminal-failure publication, transport IO, operation lifecycle, and public projection. They consume typed poll and error values; they do not infer state or error categories from diagnostic strings. One common WASM artifact contains no transport implementation.

JavaScript/WASM record-memory and OPFS ownership are defined in js-opfs-wasm-memory-layout.md. The C/WASM and shared safe-Rust inventory is documented in ../../core/crates/lace-ffi/ABI.md. The PyO3 module inventory is defined by python/crates/lace-py/src/ and python/lace/_lace_py.pyi.

Public projection

Lace-100 owns the cross-runtime application contract. Runtime projection maps core observations to exactly these public families:

Only record observations carry first or second. Round numbers, protocol sides, phase generations, fixed-point identities, store positions, wake tokens, and transport settlement causes remain internal diagnostics. Replacement creates no event.

Runtime errors preserve the shared typed categories PeerProtocol, LimitExceeded, Timeout, Policy, Store, Transport, and Integration, with public participant position, record hash, and timeout phase only when present. Rust’s existing Error preserves its primary detail and exposes additional failures through additional_errors(); std::error::Error::source() exposes the first additional failure. Clones retain failure identity for replay suppression; independently created equal diagnostics remain distinct. Combining errors preserves occurrence order and typed details before any application redaction. Errors are terminal values, not observations.

Bounded wrappers project core completion into complete, stopped-by-limit, or failed and retain observations produced before the terminal. Open wrappers project fixed-point and close boundaries while retaining the stream for later rounds. Direct and connected drivers use the same projection rules.

get, list, store, store_stream, and for_each are runtime-local operation compositions. They create ordinary private participants, drive ordinary interlace, and collect public observations and outcomes. The core RecordSaved observation carries the admitted Record; collectors and open streams take it there at poll time instead of reloading it from the private store, while bounded outcome events keep only its hash. Their collectors stay in each concrete host because they own runtime waits, IO, cancellation, and lifecycle; lace-rt and Tokio share only the pure terminal and outcome builders over already-collected observations in core runtime_values. Stores and core machines do not own porcelain collectors, and public applications receive no raw store access through them.

See ../spec/100-DX-OVERVIEW.md for exact event, outcome, close, and porcelain semantics.

Runtime realizations

All hosts preserve the same participant, operation, store, transport, and public projection boundaries while using runtime-appropriate scheduling and IO.