Lace / docs

Lace-050 · Interlace State Machine

Tags: interlace, exchange, reconciliation, state-machine

Purpose

Interlace is Lace’s record-set convergence primitive. Two participants derive one exchange plan from two canonical policy operands, advertise validated local records, request missing advertised records, validate and admit responses, and repeat until neither side requests a record.

Interlace is a sans-IO state machine, not a socket protocol. A connected carrier begins only after a host establishes a bidirectional byte transport; listening, acceptance, and service admission are outside interlace state. This specification defines direct and proof-derived session authority, carrier and logical-session ownership, establishment, symmetric round phases, snapshots, effects, fixed point, open operation, hard policy replacement, errors, and limits. 060 binds these semantics to ILTP byte streams.

Connected peer authority comes only from live protocol proof

The host fixes carrier role and connected proof inputs before one ILTP carrier starts. Listener/server is side0; connector/client is side1. Every carrier has one nonempty canonical Transport(address), one unpredictable nonzero 32-byte nonce seed, an optional local By-secret value, and one resolved proof assurance:

Disabled
ConfidentialChannel
ChannelBound(exporter32)

Each logical session begins with a fresh nonce pair and role-ordered proof establishment, then freezes immutable Here/1, PeerConfidential/1, and Transport/1 runtime facts. Here(local_key) exists when the carrier’s local By-secret value corresponds to local_key. PeerConfidential(peer_key) exists only when the live peer’s valid proof binds that logical session’s nonce pair and local assurance says the complete carrier is confidential, integrity-protected, and endpoint-confined for its lifetime. Connected hosts cannot inject final peer authority, and authority is freshly derived rather than inherited across logical sessions.

The public optional override is Disabled or TrustConfidentialChannel. Omission resolves a connected QUIB exporter to ChannelBound, canonical wss: to ConfidentialChannel, and every other standard transport to Disabled. Explicit disable suppresses proof authority; explicit trust preserves an available QUIB binding and otherwise selects ConfidentialChannel.

A same-runtime direct pair bypasses proof and receives one explicit final SessionAuthority per endpoint. Either direct value may omit all facts. Side labels remain protocol ownership labels, not identity or authority.

Fixed profile and policy boundary

ILTP 1.0 fixes:

Record formats, lowering, advertisement capabilities, parser caps, and operation budgets are not negotiated. An incompatible set requires another protocol version.

Exactly two canonical operands are the complete policy inputs. Their ordered slots deterministically derive one immutable compiled plan. Operand position derives origin and lowering scope. A compiled plan plus the endpoint’s typed local slot is the complete executable provenance. There is no host approval or third policy input.

The plan retains 040’s binary selection intersection, origin-aware whole-record exposure, request-only advertisement facts, directional vetoes, and symmetric advertisement schema. The schema cannot add selected records or widen the local exposure boundary.

Carrier and logical-session ownership

A connected carrier preserves transport role, address, proof assurance, channel binding when present, By-secret value, limits, and nonce generation. The By-secret value and nonce seed use concrete zeroizing secret owners. Changing any of these carrier inputs recreates the carrier; policy replacement changes only the operand. The carrier owns exactly one state:

Prefacing(Initial | Reset)
Running(SessionMachine, optional staged replacement)
Failed(SessionError)
Closed(CloseReason)

Prefacing owns one prepared local operand, local-preface acceptance, and the optional peer nonce. Running owns one logical session and at most one staged replacement. A logical session owns exactly one semantic state:

Establishing(EstablishmentState)
Round(RoundState)
FixedPoint(FixedPointState)
Failed(SessionError)
Closed(CloseReason)

Establishment is role ordered:

Side0Proof
Side1Proof
Side0Operand
Side1Operand
Round.Advertising

A logical session owns one current local operand: establishment owns it until plan compilation, then the immutable plan owns it. The carrier does not keep a parallel operand copy. A fresh preface pair constructs one fresh session; old and fresh session machines are never active together.

Each endpoint round has exactly one local phase:

Advertising
Requesting
Answering
Fencing

The state is the sole authority for the legal phase, round-local data, current effects, fixed-point boundary, and terminal status. Starting a round replaces all prior listings, requests, obligations, commitments, transfer state, and peer slots. Round numbers are local diagnostics and never appear in protocol data.

Each endpoint progresses independently. The two endpoints MAY temporarily occupy different local phases. Neither endpoint waits for the other endpoint to finish local work before starting the same symmetric phase. An implementation MUST NOT turn the four phases into side0-then-side1 phase ownership.

An endpoint owns at most one active machine effect at a time: one StoreCall or one retained peer output. State-local peer slots are input state, not additional effects. Polling a direct pair exposes ready effects from both endpoints together so one endpoint’s pending work does not hide the other’s.

A same-runtime direct pair receives both prepared operands and both host-supplied authority values, which may be empty, derives the same plan, and starts both endpoints in Round.Advertising. It uses the same snapshots, routing, validation, StoreCall, fences, fixed-point rules, and limits as connected operation.

Nonce, proof, and operand establishment

Each logical session begins only after the carrier has accepted its local ILTP 1.0 nonce preface and consumed the peer’s nonce preface. The role-mapped nonce texts are Challenge-side0 and Challenge-side1. They MUST be canonical B64A encodings of fresh unpredictable nonzero 32-byte values and MUST differ within the pair. The prefaces are framing and reset cuts, not semantic fact blocks.

The fresh session then follows four stable semantic phases: side0 proof, side1 proof, side0 operand, and side1 operand. Each phase receives the ordinary absolute phase deadline. A proof phase carries either one fully valid minimal Seal or one zero-row fact block. A proof Seal wraps an empty Blob and a Plex with exactly:

Group: lace
App: peer-proof
Name: session/side0|side1
TAI: 0000000000:000000000
Challenge-side0: <side0 preface nonce>
Challenge-side1: <side1 preface nonce>
[Channel-Binding-Hash: <b64a digest>]

The current proof phase fixes the sender role. No other extra field is allowed. For ConfidentialChannel, the binding field is absent. For ChannelBound, it is required and MUST match the local exporter-derived value. A sender decodes its own preface nonce and uses those 32 bytes as the Seal Mark auxiliary input. An enabled side without a By-secret value sends an empty proof but may discover a peer that presents one. Disabled sends an empty proof and derives no PeerConfidential, while still validating a received exact proof transcript. If either endpoint is disabled, the pair derives no connected peer authority.

Proof bytes are control material only. They MUST NOT be admitted, assigned a StoreRecordId, projected into facts, advertised, transferred, or emitted as application events. Proof and operand bytes count toward the logical session’s establishment received-control budget.

The two role-ordered operand Blobs then follow. Each fully validates under 010, is at most 1 MiB, contains one canonical 030/040 module, and remains setup material only. After side1’s Blob, each endpoint freezes freshly derived authority, constructs the same new immutable plan, and begins local advertising. Malformed, false, wrong-family, reflected, replayed, duplicate, or out-of-order proof or operand input is PeerProtocol; local nonce derivation or proof construction failure is Integration. Establishment has no TAI, clock-skew, expiry-window, replay-cache, operand reuse, or plan-reuse mechanism.

Peer advertisement facts

Request policy reads the current complete peer listing through:

Advertised(Record)
AdvertisedField(Record,name,index,value)

These are receiver-owned discovery claims, not checked record facts. Sender role, stream direction, session, and route provenance remain outside Datalog. The facts are available only to SelectAdvertisedCandidate/1 and private helpers reachable exclusively from that facet, as defined by 030 and 040.

For H3 fields, every advertised name/value pair satisfies 010’s complete field-line bound. Group, App, and Name values also satisfy their coordinate rules, and indexes use 020 canonical occurrence text. When the plan schema requires By, a Seal projects exactly AdvertisedField(Seal,'By','0',by_value).

One advertisement record has this shape:

Advertised('<record>')
AdvertisedField('<record>','<name>','<index>','<value>')
...

Advertised precedes that record’s field rows. Field rows contain exactly every 020 occurrence selected by the plan schema, ordered by field name bytewise and then numeric occurrence index. A schema requiring all fields includes every occurrence. Rows outside the schema are forbidden.

A listing contains each advertised record at most once. Duplicate records, duplicate (name,index) rows, noncanonical indexes, invalid coordinates, omitted required projections, false projections, and out-of-schema rows are peer protocol failures.

Each endpoint emits exactly one advertisement fact block and one request fact block per round. Fact-block framing completes the listing. A zero-row block is the canonical empty listing. A listing is one semantic block and MUST NOT be split. Partial transport writes do not change its ownership or completion.

Immutable round and output snapshots

Each endpoint freezes one coherent finite local record-fact boundary when its advertisement evaluation begins. That round record boundary supplies local record facts and StorePosition to both the advertisement and request evaluations. Each output separately freezes its other inputs and results when that output begins; later local commits cannot alter either output in the round.

An advertisement snapshot freezes:

Advertisement evaluation has no peer-advertisement fact source. A side advertises exactly its frozen 040 MaySend(Record) and Have(Record) output. A transmission veto produces an empty listing. Each selected hash, retained local identity, and complete schema projection is a frozen send commitment.

Request snapshot

An endpoint enters Requesting only after its local advertisement listing has been accepted by its next owner and its complete peer advertisement listing has been received. It atomically publishes that peer listing, starts request evaluation immediately, and freezes:

A side requests exactly the unique hashes for which:

MayRequest(Record)
and Advertised(Record)
and not Have(Record)

hold in that snapshot. A reception veto produces an empty listing. Wire requests are MayRequest('<record>') rows reporting the receiver’s frozen decision; peer input cannot define the receiver’s generated policy predicate.

An endpoint enters Answering only after its local request block has been accepted and its complete peer request block has been received. Both endpoints therefore freeze both request sets before transferring records, but they may reach that boundary at different times. A local commit after an endpoint’s round record boundary freezes cannot affect its advertisement or request in that round; it may affect a later mandatory or open round.

Symmetric round progression

Each endpoint follows these four local phases:

Advertising

Both endpoints start local advertisement evaluation independently. Each sends its complete listing as soon as it is ready. An endpoint enters Requesting after its own listing is accepted and the peer listing is received.

Requesting

Each endpoint starts local request evaluation immediately on entry and sends its complete request set as soon as it is ready. An endpoint enters Answering after its own request set is accepted and the peer request set is received.

Answering

An endpoint concurrently:

One side’s store latency MUST NOT prevent the other endpoint from starting or progressing its symmetric work. An endpoint enters Fencing when all its incoming and outgoing response obligations have settled. It does not wait for the peer to enter Fencing.

Fencing

Each endpoint emits one standalone RoundComplete() block immediately on entry. A peer fence may arrive while the local endpoint is still Answering and is retained in that round’s dedicated peer-fence slot. No endpoint emits more ordinary content for a round after its local fence.

After the local fence is accepted and the peer fence is received:

either frozen request set is nonempty -> next Round.Advertising
both frozen request sets are empty     -> FixedPoint

Every frozen request therefore forces another complete round, including a request whose admission was a duplicate. Repetition depends on the union of the two frozen request sets, not advertisement comparison, store novelty, or a result counter. If the next mandatory round would exceed the configured limit, the endpoint fails with LimitExceeded.

A fixed point does not claim that stores are equal. It means neither side requested a record in the settled round under this plan and these snapshots.

Transfer commitments and validation

Each frozen request creates exactly one response obligation. The known request set determines response-phase completion; there is no response-end control. A response is one complete stored record whose glyph hash identifies the matching outstanding request.

The sender loads the matching frozen commitment by retained StoreRecordId. It MUST NOT reevaluate selection, exposure, or directional vetoes. A missing row, wrong hash, or different immutable bytes at that identity is a terminal store or integration failure.

The receiver validates the response’s requested hash, record structure, embedded hashes, and Seal Mark. It projects the record through the plan schema and requires exact equality with the remembered advertisement record. Invalid bytes, an unrequested record, wrong hash association, or any projection mismatch is a peer protocol failure and MUST NOT produce record facts.

A valid response is admitted through the ordinary store path before its obligation settles. Duplicate admission settles successfully. A response phase completes only after every obligation has a received, validated, and settled response. There is no successful per-record negative response.

RecordSent is produced when a committed response is emitted. RecordSaved is a local observation after valid receiver admission settles. Fixed-point entry and clean closure are one-time lifecycle observations. These observations do not change protocol state or publish store positions. Policy replacement has no observation or activation event.

State-local peer input and backpressure

Each round state owns only the peer slots required by its current progress:

Per-direction stream order permits bounded phase skew. A matching peer listing may fill its dedicated slot while local evaluation is pending. A peer fence may fill its dedicated slot while local Answering finishes. At fixed point, one peer advertisement fills the next round’s peer-listing slot and starts that round.

Input is validated immediately against the owning state. Duplicate, skipped, wrong-family, unrequested, or otherwise out-of-order input is a peer protocol failure; it never enters a general semantic queue. At most one complete decoded item may wait for immediate machine handoff.

A connected host reads or decodes semantic input only while the current state has a legal free peer slot and no StoreCall is active. While StoreCall is active, the endpoint consumes no peer input, starts no other effect, and relies on transport backpressure. A WebSocket adapter similarly stops decoding and retains only bounded transport data. Transport-buffer overflow is a transport failure, not a semantic response queue.

StoreCall ownership

A sans-IO endpoint owns at most one active opaque StoreCall. It contains machine identity, side, request data, the suspended continuation, response construction, and exactly-once submission state. While it is active, that endpoint accepts no peer input or policy replacement and starts no second effect. A direct pair may own two concurrently, one per endpoint; either sibling may otherwise continue independently, but routing into a blocked endpoint waits.

Polling exposes borrowed request and response-builder access to that same object. The host snapshots request data, executes it against the endpoint’s policy-neutral store, reacquires the active StoreCall, fills its response, and submits once. It does not create another response owner. Wrong machine, wrong kind, incomplete coverage, and duplicate completion are integration failures. Pending execution never becomes empty facts or a store outcome.

Exact-load and store results are positional against the ordered request retained by StoreCall. Load outcomes contain loaded record bytes or Missing; store outcomes contain disposition and returned StoreRecordId. They do not echo requested hashes, requested identities, input hashes, or response slots. Loaded bytes are still validated against their requested or frozen expected hash. Broad-query discoveries, current StorePosition, and typed store errors remain actual result data.

The runtime-owned Lace that adopted the store owns mutating execution through durable-outcome classification, current-position refresh, current-StorePosition publication, and response submission. A store error remains an error even if durable progress occurred; the Lace still publishes confirmed progress once.

Every terminal transition retires the active StoreCall in its semantic owner: a connected SessionMachine retires its endpoint effect, and a DirectPair retires both endpoint effects. ConnectedCarrier does not own or separately retire session-held StoreCall. If cancellation, timeout, peer failure, local failure, or owner retirement wins after execution starts, non-cancellable work may finish while retaining its owning Lace long enough to classify and publish confirmed durable progress, but it MUST NOT submit to the retired machine. Later polls MUST NOT expose stale StoreCall; terminal retirement belongs only to the semantic owner.

Peer-output ownership

An endpoint owns at most one retained peer output. Polling borrows that output; it does not clone the item list.

A direct pair may accept one output in each direction concurrently. One direction MUST NOT block acceptance in the other. A connected writer owns one complete encoded batch and offset until every byte is written; a batch may contain one or more ILTP items from one accepted machine output. Machine phase may advance after output acceptance, not socket flush, but bounded completion and clean close MUST NOT be published until the final fence writer is empty. On replacement, an accepted old batch drains completely, unaccepted old output is dropped, and the fresh preface is the next accepted output; proof and operands cannot overtake it. WebSocket enqueue is not final flush; the host retains ownership until buffered output is empty.

A terminal failure drops retained output and closes the connected transport immediately, including after a partial write. No terminal protocol item is sent or completed.

Round-scoped concurrent commits

An interlace does not pin a logical-session-start store snapshot. Each endpoint obtains one finite coherent StorePosition boundary when its advertisement evaluation starts in each round. Its advertisement and request evaluations both use that boundary.

A local record visible at the round boundary may affect either output. A commit after that boundary cannot alter either output in the round. An unrelated external commit does not force an additional active round. Frozen requests force repetition independently of store novelty.

Fixed point and open operation

FixedPoint is the sole successful settled protocol state. Every entry creates one fresh opaque FixedPointId, produces one fixed-point observation, and contributes one FixedPointWakeBoundary to the machine’s FixedPointBoundary. The FixedPointBoundary contains the settled round and one independently tokened wake boundary per endpoint. Each wake boundary contains that fixed-point identity and the terminal round’s local record-fact StorePosition boundary.

Later polls wait; they do not reproduce the entry observation.

Fixed-point entry commits the semantic result, but a connected bounded host does not return until required final output and transport settlement finish. For a byte-stream route it stops peer input, flushes final output, and closes or releases the route. For QUIB, peer-settlement authorization is legal only after fixed-point entry, the ILTP codec writer is empty, and every final ILTP byte has entered the QUIC send stream. Codec acceptance alone is insufficient. The bounded host then requests QUIB graceful finish; an open host may grant the same one-way authorization without beginning local finish. If a peer preface linearizes before this boundary, the host follows replacement instead.

After QUIB authorization, peer application close with code zero and authenticated stateless reset are successful but distinct transport settlement causes: PeerClose and PeerStatelessReset. Both preserve the committed semantic result. On an open operation either cause also delivers boundary EOF exactly once, producing Closed(PeerClosed). Before authorization, orderly peer close is premature EOF and authenticated stateless reset is a transport failure; neither may be reclassified from fixed-point knowledge alone. Nonzero application close remains a transport failure.

QUIB owns final data and FIN retransmission, acknowledgement, normal close, and drain after authorization. Settlement completes through the route-removal stages defined by 060: the connection emits RouteDrainEvent, the endpoint emits RouteRemoval, and the host removes the exact route before confirming removal to the connection. Post-fixed-point QUIB settlement uses QUIB’s transport deadlines and host cancellation, not the interlace phase timeout and not a new operation limit.

An open host uses each Lace-owned current-StorePosition watch. It closes the registration race in this order:

  1. subscribe to the current-StorePosition watch;
  2. install the endpoint’s FixedPointWakeBoundary from the reported FixedPointBoundary;
  3. read the watch’s latest position; and
  4. sleep only when the latest position is not greater than the wake boundary’s position.

A local position advance starts Round.Advertising immediately. The resulting advertisement block is the peer’s round-start notification. A peer advertisement received at fixed point also starts Round.Advertising, stores that listing in the new round’s peer slot, and starts local advertisement evaluation concurrently. Crossed advertisements naturally belong to the same new round. Neither role coordinates or has priority for an open round.

A watch wait captures the complete boundary, including FixedPointId. Delivery is legal only while the endpoint owns that exact fixed point. If a peer advertisement, logical-session replacement, or another transition has retired it, the host discards readiness for that stale boundary; the active output snapshot or next fixed-point watch covers any store commit.

Ordinary item-boundary EOF at fixed point transitions once to Closed(PeerClosed) and is clean for bounded and open hosts. A QUIB close is submitted as that boundary EOF only under the authorization rules above; an unauthorized close is premature even if the semantic machine has entered fixed point but final-byte preconditions have not settled. Cancellation transitions once to Closed(Cancelled). Optional local work after a bounded peer closes remains eligible for another session.

Hard policy replacement

Hard replacement changes only the executable local operand. Preparation occurs before touching the carrier or direct pair. Applying an operand canonically equal to the running, prefacing, or already staged operand is a successful no-op.

On a connected open interlace, replacement is synchronous staging through the same exclusive mutable owner that drives the interlace. Success replaces the operand in Prefacing or stores at most one replacement in Running; a later successful call may supersede it before stepping. Staging MUST NOT poll or retire a machine, read peer input, execute StoreCall, touch the writer, deliver or create observations, or mean that the replacement has activated.

At the start of the next ordinary carrier poll or step, before endpoint work or fixed-point watch delivery, a staged replacement atomically retires the complete logical session, drops machine-owned unaccepted output and unsettled observations, and enters reset prefacing with the staged operand and a fresh local nonce. An already writer-owned old batch drains before the fresh preface. While awaiting the peer preface, complete old peer items are canonically decoded and discarded under the received-control budget. Malformed framing remains terminal because a preface cannot cut through an item.

A fresh peer preface delivered to a running carrier performs the same retirement, extracts the current role-owned local operand, enters reset prefacing with the received peer nonce and a fresh local nonce, and emits its preface after any old writer batch drains. A preface received during proof, operands, rounds, or fixed point is therefore a hard reset. Crossed replacement forms one preface pair; a second peer preface before construction of the fresh machine is a duplicate peer-protocol failure. Replacement while already prefacing only replaces the forming local operand and emits no additional preface.

Prefacing has one absolute deadline covering writer drain, local-preface acceptance, canonical old-input discard, and peer-preface receipt. Once both prefaces are owned, the carrier constructs one fresh machine at Side0Proof. The fresh logical session repeats proof, complete operands, immutable plan construction, and ordinary rounds. It derives fresh authority and accounting; there is no rollback, reset acknowledgment, generation number, mutable plan, or old/fresh machine overlap.

Endpoint-exclusive StoreCall prevents staging or peer-preface delivery to that endpoint until execution, durable-position publication, and response submission settle. A direct replacement waits for both endpoint StoreCall effects when present. At the next direct poll or step it atomically retires both endpoints and their plan, drops unaccepted typed output and machine-owned observations, and constructs a fresh direct pair from the fully prepared operand pair with the same explicit authorities and limits. Direct replacement has no wire preface or per-slot operation.

EOF and failure

EOF during prefacing, establishment, an active or mandatory round, or QUIB post-fixed-point settlement before final-byte authorization is premature and is a peer protocol failure. EOF in the middle of a preface or codec item is framing truncation and is also a peer protocol failure. An authenticated QUIB stateless reset before authorization and every other transport error remain transport failures. Authorized post-fixed-point zero-code close and stateless reset are the two settlement causes defined above, not failures. Valid records already durably admitted remain after failure.

Terminal errors use these categories:

Malformed or hard-cap peer input is PeerProtocol, not a local limit. Evaluation work, output size, transferred-byte accounting, and mandatory-round budgets produce LimitExceeded. A timeout is not a resource limit.

A connected host closes transport immediately on local terminal failure. The peer may observe transport failure, framing truncation, or premature boundary EOF. No wire error report is promised.

Deadlines

Deadlines are host-owned and attached to stable carrier-prefacing or endpoint phase tokens. Each token receives one absolute deadline on entry. With active StoreCall, the connected host races only that execution, deadline, and cancellation; otherwise it races legal peer input or EOF, retained write progress, deadline, cancellation, and any legal fixed-point watch. Event reads, partial IO, replacement staging, and progress by another endpoint do not reset or extend a deadline. Fixed-point waiting uses store watches rather than a phase deadline. Authorized QUIB finish, close, and exact-route drain likewise do not reuse a semantic phase deadline.

Limits

Portable hard caps are fixed:

Item Hard cap
fact line 1024 bytes excluding LF
ILTP item 64 MiB
operand Blob 1 MiB
operand Blobs sent per direction per logical session 1
H3 Blob data 32 MiB, from 010

One operation also has local configurable lifecycle limits:

Limit Default Scope
max received control bytes 64 MiB one received direction and prefacing, establishment, or round
max transferred record bytes 1 GiB one transfer direction and round
max evaluation work 10,000,000 units one evaluation
max mandatory rounds 16 rounds per logical session before a fixed point
phase timeout 30 s one prefacing or endpoint semantic phase

These values are local and are not exchanged. Each connected host owns its carrier’s operation limits; one direct operation owns one limits value for the pair. Implementations MAY configure lower operation values but MUST support the defaults. Fixed hard caps are not configuration choices. Replacement starts fresh logical-session accounting without changing configured values or adding a replacement limit.

Evaluation work counts logical candidate visits, projected fact bytes, join attempts, builtin tests, deduplication probes, and distinct aggregate witnesses. Store pages, allocations, and host scheduling do not define logical work. Internal scheduling bounds yield and resume; they are not outcomes.