Contents

Store call and work binding boundary

Bindings need to execute semantic store requests without retaining a machine borrow across a thread or await. This note defines the shared binding layer used by C/WASM and PyO3 for the operations owned by store-record-fact-access.md.

StoreCall remains machine-owned

Each endpoint machine owns at most one active StoreCall. It contains the suspended continuation, ordered StoreOp, and an empty or filled complete response. Submission consumes the active call exactly once. A connected interlace therefore has at most one; a direct pair may have one per side concurrently.

Native Rust code may borrow StoreCall within one machine turn. No foreign API exposes its address. A raw StoreCall pointer is not a durable identity: a borrow expires on machine mutation and an allocator could later reuse its address.

The shared binding layer in the lace-ffi rlib provides safe typed Rust APIs and explicitly unsafe trusted-record byte commits. Thin C/WASM exports and PyO3 wrappers use that same layer. It owns value projection and identity checks only—never a store, Lace, mutation gate, watch, future, worker, callback, or runtime lifecycle.

One owned StoreWork crosses waits

A binding issues one move-only execution value for an active side of an interlace machine or for a retention sweep:

StoreWork
├── hidden issuer: interlace-machine identity and side, or sweep identity
├── hidden nonzero StoreCall id
├── owned StoreOp
└── empty or complete owned Result<StoreResult,StoreError>

The C/WASM spelling is an opaque owned allocation:

typedef struct LaceStoreWork LaceStoreWork;

LaceStoreWork *lace_interlace_machine_issue_store_work(
    LaceInterlaceMachine *machine,
    uint32_t side,
    LaceErrorOut *error);

uint32_t lace_store_work_side(const LaceStoreWork *work);
uint64_t lace_store_work_id(const LaceStoreWork *work);
uint32_t lace_store_work_op_kind(const LaceStoreWork *work);

int32_t lace_interlace_machine_submit_store_work(
    LaceInterlaceMachine *machine,
    LaceStoreWork *work,
    LaceErrorOut *error); /* consumes work */

void lace_store_work_free(LaceStoreWork *work);

Normal routing uses protocol side; sweep work reports side 0. The sweep’s lace_retention_sweep_issue_store_work/_submit_store_work pair follows the same rules (ABI.md). The id is available for diagnostics and tests; callers do not reconstruct a ticket. The binding issues at most one work value for one active ticket. A second issue is an integration error even if the first value was freed. Abandoning work while the operation remains live requires terminating that operation.

StoreWork is immutable while another thread executes it except through its exclusive result-construction calls. It may be moved across one thread boundary or retained as an opaque WASM pointer across await, but callers must not access it concurrently. PyO3 exposes the same move-only Rust value; its machine and codec wrappers may move between threads, and PyO3 borrow checking rejects concurrent calls.

In raw WASM the numeric pointer denotes the owned Rust allocation, not StoreCall. It remains opaque across memory.grow. After a result-byte reservation, the host reacquires memory.buffer, fills exactly the reservation, and makes commit the next call on that work value.

Request accessors

StoreWork accessors need no machine argument and expose one owned operation:

CurrentStorePosition
ResolveStoreRecordIds
FindRecords
LoadRecordsByHash
LoadRecordsByStoreId
StoreRecords

They are positional and operation-neutral: ordered request entries (hash, store id, or record bytes), read_through, lower_exclusive, resume cursor, row bound, and one ordered sequence of exact-hash, field, field-prefix, and record-link constraints read by slot and text part. No accessor or result builder is named after an operation; the operation StoreWork holds selects the meaning. Actual StoreRecordId inputs reject zero.

PyO3 projects a FindRecords query once into owned Python values used directly by the host’s StoreQuery. Constraint order, duplicates, and absent versus empty values survive projection and StoreWork consumption; C/WASM returns an absent text part as a null pointer and empty text as a non-null pointer with zero length.

Containment, exposure, Datalog, and policy do not cross this boundary. Rust projects facts and applies those constraints above storage.

Work-owned result construction

Foreign result calls mutate only StoreWork. They never incrementally mutate the live machine. Their families are:

CurrentStorePosition
ResolvedStoreRecordIds
FoundRecords
LoadedRecords
StoredRecords
StoreError

Exact outcomes are positional against the StoreOp retained by StoreWork:

ResolveStoreRecordIds[i] -> StoreRecordId | Missing
LoadRecordsByHash[i]     -> Loaded(record bytes) | Missing
LoadRecordsByStoreId[i]  -> Loaded(record bytes) | Missing
StoreRecords[i]          -> Stored(StoreRecordId) | Duplicate(StoreRecordId)

Outcomes do not echo requested hashes, ids, input hashes, or response slots. Construction calls use a slot only to fill the corresponding position. The complete result retains no slot value.

C/WASM builders set a position or completion flag, add an id-only entry outcome, or reserve and commit one entry’s record bytes. Full-record reserve/commit buffers belong to StoreWork until it is consumed or freed. Their trusted-byte preconditions are defined in lace-ffi/ABI.md. The Rust StoreWork raw-byte commit methods are explicitly unsafe under those same preconditions; request/hash checks do not replace prior record validation. The safe typed entry-record method, used by PyO3, instead accepts validated Record owners; found hashes come from those records, while exact loads still check the requested or frozen expected hash. Arbitrary Python bytes enter through Record.from_bytes, not store-result construction. The Python host clones its existing record owner into the same work-owned result builder without serializing or reparsing it; heap and native mmap backing remain shared. Typed fills use the same result family, positional coverage, and submission validation as byte transport.

Equivalent construction covers found rows, resolved identities, store dispositions, post-store high-water, and one explicit whole-request StoreError. Pending is host scheduling state, not a StoreResult.

Before touching the machine, the shared binding layer validates the complete owned result against its StoreOp:

Invalid or partial construction is an integration error and leaves machine StoreCall unchanged.

Atomic ticket-checked submission

Submission consumes StoreWork on success or failure. It verifies the hidden issuer, active StoreCall id, and exactly-once state; moves one already-validated Result<StoreResult,StoreError> into StoreCall’s complete response; and submits once, consuming the active call. Wrong-issuer (including sweep work submitted to an interlace machine and the reverse), stale, incomplete, and repeated submission are integration errors. No pointer equality participates.

Policy replacement uses the same exclusive machine owner and remains rejected while StoreCall is active. While one endpoint is blocked, its direct sibling may continue independently.

Immediate, asynchronous, and late execution

Immediate and potentially blocking foreign stores use the same work shape:

issue StoreWork
→ host executes its owned StoreOp
→ construct one complete owned result
→ for mutation, classify durability and publish through the owning Lace
→ the operation driving the machine submits StoreWork

An indexed-memory primitive executes work synchronously. OPFS, native filesystem, and custom stores may move it through a JavaScript promise, Python worker thread or an application-owned native executor. Only scheduling differs. Rust records remain Arc-backed in StoreOp and StoreResult. Native filesystem records retain Arc<MmapRaw> plus one range that was admitted by the immutable record-log prefix; no safe arbitrary mmap range crosses this boundary.

Every semantic terminal transition retires StoreCall in its actual owner: DirectPair retires both endpoint effects and connected SessionMachine retires its endpoint effect. ConnectedCarrier, FFI, and host adapters expose no discard hook. If timeout, cancellation, failure, or drop wins after work starts, a late read frees its work without submission. A late mutation first refreshes and publishes any confirmed durable position through its owning Lace, retains any store, refresh, or publication failure on that Lace, then frees work without submission. Refresh failure is Store; publication failure is Integration; neither permits successful mutation submission. StoreWork itself never publishes, schedules, or calls back.

Concrete Rust store primitives

Foreign builds expose one opaque policy-neutral Rust store that executes StoreWork. It holds a lace-sans-io StoreBackend implementation; only its constructors name a backend:

LaceStore *lace_store_memory_new(void);
LaceStore *lace_store_fs_open(const uint8_t *path, size_t path_len, LaceErrorOut *error);
LaceStore *lace_store_lmdb_open(const uint8_t *path, size_t path_len, LaceErrorOut *error);
int32_t lace_store_execute_work(LaceStore *store, LaceStoreWork *work, LaceErrorOut *error);

The native filesystem and optional LMDB constructors accept only the root path. Cargo native-fs enables lace_store_fs_open on non-WASM/non-WASI targets; C consumers define LACE_FFI_FEATURE_NATIVE_FS only when linking that feature. Cargo lmdb and LACE_FFI_FEATURE_LMDB similarly enable lace_store_lmdb_open on its supported native targets. Neither constructor supplies disabled stubs or WASM exports. PyO3 enables its own optional dependency and wraps the same concrete backend directly, not through C calls. Both fill existing StoreWork results rather than defining another operation protocol.

The ABI’s store-primitive contract defines scalar current-position and execute-work status/error semantics. Execute-work status describes result installation, not whether the installed result contains a backend StoreError; ordinary machine submission consumes that result.

A native C object owns its store directly. PyO3 has one NativeStore class owning a Mutex<Option<Box<dyn StoreBackend>>> with indexed-memory, filesystem and LMDB constructors; its inline property reports the backend’s execution class. Each execute_work installs the complete result and returns the store error text that result carries; only failure to install raises. Built-in store objects are reachable only through their Lace. For each backend, native C and PyO3 execute the same concrete store implementation. Filesystem primitives retain exclusive lifetime root ownership; LMDB primitives permit cooperating processes. No lock, transaction, posting-list handle, refresh scheduler, or separate opener identifier crosses FFI.

Native execution fills the work-owned result but does not submit it or publish position. PyO3 detaches from the GIL around native open, StoreWork execution, authoritative position reads, and consuming close; the thread driving the operation, not a store worker, publishes position and submits to the machine. Returned native records retain Arc<MmapRaw> and their accepted immutable log-row range across store close and foreign result ownership. Primitive destruction releases only directly owned resources, performs no posting-list checkpoint, and is not Lace participant close. A one-shot indexed-memory builder may seed finite providers from validated records; it is not a public mutable store API.

Required boundary tests

Keep focused tests for every request and result family, operation-neutral export names, constraint order/duplicate/absent-versus-empty projection, two simultaneous direct side work values, mixed side effects, one-work-per-ticket enforcement, move-across-thread/await use, immediate memory execution, exact-ticket submission, wrong-machine and stale work, incomplete/invalid result atomicity, replacement rejection while StoreCall is active, positional loads/stores without identity echoes, missing and malformed records, uncommitted reservations, durable publication on mutating errors, explicit StoreError propagation, late completion without submission, reservation cleanup, Arc-backed record preservation including native raw-mapped records after store close, PyO3 GIL release around blocking native work, primitive destruction without a checkpoint, and host side routing without a context or registry.