Files
Endoleap/headless/README.md

82 KiB
Raw Permalink Blame History

Headless Canvas: portable ESM preview

Build from the Endoleap checkout with npm run build:headless. The generated .headless-dist directory is a private package; nothing is published to a registry. Copy it elsewhere, or use npm pack --offline --ignore-scripts inside that directory.

Public exports:

Import Responsibility
@endoleap/headless-canvas or /core Document, registry, capabilities, transactions, history, sessions, geometry and queries
/runtime Project/Workspace/Host, storage and service port types, recovery, scoped loader and compute/display pools
/service Foundry protocol codecs and transport adapters; credentials remain in the host
/browser Explicit IndexedDB/Web Locks and module Worker adapters
/bindings Portable projection, renderer descriptors, caches, connectors and hit queries
/react React Provider and selector, element, action, mode, gesture, operation, asset and sync hooks; no React Flow runtime dependency
/react-flow Explicit client adapter, renderer command context and framework node/edge types

The portable entries have no React, Next, React Flow or other runtime package dependency. They use standard platform APIs such as AbortController, Blob, URL, TextEncoder and Web Crypto. The sample runs on Node 22+. Browser adapters require their browser APIs; importing /runtime does not import the browser adapter.

Run node consumer.mjs inside the generated package. It uses a custom counter component, rejects an atomic invalid transaction, commits edits, undoes/redoes, cancels a gesture preview, deep-copies through the runtime, and reopens the engine. The storage and protocol-2 transport are in-memory examples. Reopening demonstrates the codec and host contract, not cross-process crash durability or a deployed server.

npm run test:headless:package builds, packs/unpacks into an isolated directory, blocks framework imports and browser-only globals, runs the sample, checks Node TypeScript consumers without the DOM library, and exercises compiled browser adapters and module Worker URLs in isolated Chromium. The browser checks load the emitted JS directly, without TypeScript stripping or a Next bundler.

The React adapter has an explicit entry and optional peer dependencies; portable consumers do not load it. No registry publication, release support promise, server deployment or full-goal completion is implied by this local package.

Explicit component-data migration

migrateElement(record, registry) prepares a detached record; it does not write the document or migrate storage on read. The ID, incarnation, component and owner stay unchanged. Each version converter receives frozen detached data and must return a JSON object. Missing converters, thrown reference mapping, invalid schema, undefined/function/non-finite values and malformed object/array data reject rather than being erased or coerced by JSON serialization. Field values are captured once, then frozen, so a getter cannot return one value for validation and another for serialization. Unknown components and future versions remain unchanged. Use an editor transaction when several prepared migrations must publish together: an error after a staged edit leaves document, history, operations and indexes at their original Root. Progressive imports still use their declared per-record failure boundaries; this helper does not promise whole-import rollback.

Dissolving an owning canvas

Dispatch {type: 'canvas.dissolve', canvasId, entry: {id, incarnation}} with the current entry identity. The root cannot be dissolved. The entry remains an ordinary frame with its existing incoming/outgoing connections. Direct members move to the parent; nested entries are rebound there, while their own members retain their owners. Node world position, rotation, size and visual units, and connection curve positions are preserved. References declared as canvas IDs are redirected from the dissolved canvas to its parent; literal strings and opaque payloads are not guessed or rewritten.

The command requires movable known members and editable declared reference fields. It checks the host policy for each member move/reference edit and rejects stale entry identities, unsupported curve conversions or policy/schema failures before publishing anything. A partial source must be completely captured before dissolving; it never promotes only the members loaded so far. All changes publish as one history/ledger unit and use the existing atomic protocol-2 edit path. Undo restores the owning canvas, placements and references; redo dissolves it again. No member/entry is deleted or given a new incarnation by this operation.

Experimental partial-model runtime: pass a validated prefix plus loading in EngineOptions. Version 1 uses recovery format 7; version 2 carries snapshot-bound direct canvas membership counts and uses format 8. Both keep the scheduler held. Version 3 uses format 9 and retains runtime.loadingRead separately from the moving ACK base. Loaded non-reference data fields and node geometry can be edited and undone; creation, deletions, ownership/reference changes, uploads, imports and AI admission wait. engine.loading.accept(document, versions, state) extends the captured prefix without replacing local edits. engine.loading.complete(document, versions, {sourceToken}) validates the full capture and releases scheduling only after its cut is durable. Use {fresh:true} only for an explicitly fresh complete read; conflicts require the existing reviewed recovery flow. engine.loading.resume() coalesces a fresh complete read through the project's persistence adapter and persists completion before resolving. workspace.resumeLoading(projectId) delegates to that owner and reports current-identity failures. Closing the project aborts the read and saves the recoverable prefix; detaching a view does not close its engine.

Endoleap's page now opens the same project engine for a validated prefix, allows the restricted field/geometry edits above, coalesces incoming prefixes, and preserves its textarea, history and viewport when the full model arrives. App navigation cancels the view's stream and asks the retained project owner to complete through a fresh read. Reload recovers the durable prefix when that project is opened again. This is not a persistent HTTP cursor or a workspace-wide startup scan. Version-3 scoped ACK/chunk coordination and later scope-based structure/import permissions are described below.

Scope-aware queries are obtained from engine.editor.getQueries() and share the same publication boundary as the root. lookupElement(id) returns present, absent (complete project), or unloaded; a missing ID in a partial model is not proof of absence. membersResult(canvasId) adds a completeness flag for direct members; childCanvasesResult covers published topology, while incidentResult requires the complete project because another owner may contain an incoming edge. Legacy array-returning methods enumerate loaded records only. Hit-query completeness also includes scope coverage. Derived indexes and coverage views are not persisted.

An optional canvasElementCounts field in the version-1 stream header allows new clients to certify individual scopes. Missing metadata retains legacy conservative coverage. Counts include nodes and connections; they must cover exactly the declared active canvases, sum to the total, and never be exceeded by received records. A scope is ready only after its full count has entered the dependency-closed published prefix. Version-2 loading permits connection.path edits and history in a certified owner scope; endpoint, ownership, creation and deletion edits still wait. Scope completion never replaces terminal/EOF validation for the whole project. Versions 1/2 continue to hold network scheduling. In version 3, mutable operations wait in the scheduler's derived admission set until every participant scope is complete and its root is durable; unrelated certified scopes can send meanwhile. The normal frozen-body/key, writer claims and pre-dispatch durability barrier apply.

runtime.loadingRead retains the original source document and version vector. Incoming prefixes and same-source completion validate against that capture; scopes with advanced ACK versions retain their newer acknowledged records, then pending intents replay. Workspace.open(document, versions, {sourceToken}) completes that original stream, even when its captured version is now older than a scoped ACK. An unrelated fresh read waits for outstanding receipts and uses a scheduling hold while capturing and persisting its base; edits during the read remain local and are replayed.

Recovery format 9 encodes both baselines and original frozen requests. Unknown results still query/retry the same body/key after reopening. Partial-source completion clears the retained read baseline and forces a full checkpoint without older partial journal dependencies. Creation, deletion, owner/endpoint/reference changes, uploads and AI remain gated during partial loading; broader structural scope admission and real Foundry deployment/integration remain unfinished.

Loading version 4 adds an optional stream-header elementOwners manifest and uses recovery format 10. It covers every active source ID, including unloaded records, and must match both per-owner counts and each received record. New IDs must also avoid the captured tombstones; new canvas IDs avoid all captured version keys. Without this manifest the page retains version-3 field-only admission.

In version 4, complete owners can create elements and a frame/child-canvas pair, undo/redo them, redirect loaded connections, and move/remove objects born after the capture when the affected owners are ready. Existing-element removal/reparenting requires the entire captured source closure; a missing incoming edge is not proof that no edge exists. Declared references are checked against the transaction's final model. Shared loaded asset references are permitted; new uploads, generation, complete export remain gated in version 4. Version 5 adds import/copy admission as described below.

ACK validation reconstructs the exact protocol-2 effects of applied logical intents on the original source; late source blocks cannot erase births, restorations or tree changes. Both the source capture and exact request identities recover through format 10. Pure intent proofs and applied prefixes are cached by immutable source/registry/ intent identities; changing the source invalidates those proofs.

Query coverage also exposes an immutable captured ID set. A missing ID outside that complete namespace can be reported absent before all payloads arrive; an ID inside it remains unloaded until received. An old tombstone alone cannot classify a captured restored ID as absent. Absence does not make an old ID available for fresh creation. The identity manifest costs O(N) header bytes and memory and still obeys the existing 8 MiB line / 64 MiB response limits; this does not make database acquisition lazy.

Loading version 5 (recovery format 11) also retains the complete captured sourceCanvases topology from the existing stream header, even for partitions whose entry payloads are not published yet. With counts and elementOwners, the page now selects this version. Existing v1–v4 recovery images retain their original contracts; future format 12 is rejected.

engine.imports.canStart(canvasId) and canDuplicate(selection, {viewCanvasId}) return availability for UI controls, including policy and lifecycle checks. The actual methods recheck admission. Native NDJSON import requires a complete target owner. Deep copy requires complete selected subtrees and captured ancestor owners, so upper-canvas incoming edges are copied from the same original endpoint into the new partition. A declared but unpublished child is not an empty partition. Unrelated incomplete partitions do not prevent these jobs from starting.

The existing ImportJob/source Blob/runner, history and scheduler are reused. source.loadingAdmission records the source token, latest captured logical operation sequence, and sorted required canvas IDs. On partial recovery it must match the retained read and proved scopes. Before copy planning/resume, the source topology is checked against that historical logical cut, independently of newer source chunks and local edits. Source SHA, stable mappings and generation checks continue to protect payloads and late callbacks. This is provenance metadata, not another queue or a lock held during parsing.

Pause/reopen/resume retains source bytes and identities. Cancellation keeps already published content and does not restart; undo remains independent of the source parser. All content sends through the normal durability and frozen-request barriers. Full-source completion removes the loading marker and read baseline without replacing job/history state. In version 5, new image uploads and generation remain gated while loading; loaded shared resource references obey the existing structural rules.

Loading version 6 (recovery format 12) admits local images in complete owners. engine.canStageFiles(canvasId) supplies UI availability; stageFiles rechecks actual transaction changes and policy. Existing field/geometry and import/copy contracts continue to apply, including copying a pending image by the same ResourceId rather than starting another upload.

ResourceRecord.loadingAdmission records {version:1, sourceToken, sourceURL, canvasIds}. The resource's original upload OperationId precedes its first document use. Validation checks that first use's exact owner set, complete captured/born scopes, and the adapter's URL interpretation on reopening. New resource references are admitted from these proved identities and the current staging transaction; arbitrary resource IDs/URLs remain rejected. The proof and file bytes use the existing project recovery store, not a second queue or workspace-wide AssetDepot.

Working content, history, upload intent and Blob bytes retain their existing atomic durable cut. An upload has no canvas participants, so its own admission does not claim a fake canvas writer; it still waits for the root/file/request barriers. Document requests depend on stored upload results and keep normal owner/version claims. Upload receipt validation does not advance document versions. A lost ACK reopens the same frozen key/body and queries it before another document send.

Whole-source completion preserves pending/stored resources and history. Default image sizing and display handles reuse the existing decoder/cache lifecycle. Undo/redo, source failure, quota retry and late chunks do not re-upload bytes or restore deleted incarnations. Earlier loading/recovery versions retain their old restrictions; current recovery compatibility is described below. Generation, cross-project durable depot sharing and broader lifecycle/acceptance work remain separate unfinished goals.

The package now exposes /bindings for pure projection, renderer descriptors, geometry/projection caches, hit queries, connectors and resource-view handles. projectCanvas consumes the captured Document/Registry/queries and optional gesture geometry. It returns view DTOs; it does not own another document, history or queue. Projection DTOs and compatibility card/edge hints no longer import React Flow types or the legacy element classes. Legacy views re-export the same shared data types.

/react-flow is the explicit client/framework entry. It exports renderer factories, the command context/hook, and ReactFlowProjectionNode/ReactFlowProjectionEdge structural adapter types. React, React DOM and React Flow are optional peers at the package level, required when a host opts into this entry. Core/runtime/service and pure bindings remain usable without them. The declared floors are React/React DOM 19.3.0 and React Flow 12.12.1; tests use the locally installed compatible versions, not a claim to have tested every allowed version.

A host retains the immutable renderer catalog and creates view-local projection caches; installed components submit commands through its scoped Provider. Attaching or removing UI does not replace the ProjectEngine or terminate durable jobs. Compatibility DTOs are rendering hints, not the model schema or a migration format. The existing application now uses the same explicit framework entry.

The packed Node consumer projects and hits its custom counter using only public exports, with DOM globals and external runtime dependency resolution blocked. Its NodeNext type check excludes DOM libraries. A separate peer consumer renders a registered component, verifies its command context, and checks typed NodeProps and EdgeProps with the declared peers. Browser tests still execute the compiled ESM storage and Worker artifacts independently of Next.js/TypeScript stripping.

Workspace asset depot and file storage:

createWorkspaceRuntime owns one depot when StoragePort.assetDepot is provided. await engine.adoptResource(id, (resource, tx) => { ... }, {signal}) obtains fresh server permission, verifies the original bytes, and runs a synchronous editor transaction. The callback uses the existing resource.id / canonical resource.src in a newly allocated element; it also receives server dimensions in resource.asset. The project stores a SharedAssetBinding, not a second upload. Binding, document, history, ledger, shared bytes and pins cross the same durable barrier. The returned local Result does not imply durability or backend completion. Async or failed callbacks leave no partial binding. The Foundry authority requires the matching permission-checked asset metadata endpoint; cached metadata is not an authorization.

Bindings use recovery envelope 13 (including partial loading v6); readers retain formats 1–12 and reject unknown future formats. A reopened binding can display shared local bytes, and undo/redo reuse the asset without uploading again. The workspace closes projects before its depot; retiring the identity fences late work.

The browser adapter uses IndexedDB version 5 (files retain their version-1 pointer format). After a proven stored-upload cut, a project's files row holds an opaque pointer to digest-addressed asset-bytes. Unconfirmed uploads and import source files keep their original Blob rows. Use readFile / scanFiles; direct assumptions that every raw files-store row is a Blob are invalid. Logical reads preserve the original local MIME, even if the server detected another MIME. Existing v3 databases upgrade without dropping checkpoints or original files and promote on a subsequent matching Root commit (or an idempotent reindex). Older DB readers reject v4. Blob URLs and already returned Blob objects live independently of a later persistent eviction; actual disk reclamation remains the browser's responsibility.

Resource identities retain immutable metadata after byte eviction. Complete, revision-matched durable manifests from every project in the workspace protect closed editors; missing/stale/opaque coverage prevents shared deletion. Distinct resource IDs may own the same digest; the shared byte row disappears only when its last live identity is retired. An older Root cannot release temporary bytes that belong to a newer adoption revision. Collection is explicit and local; this preview does not enable background deletion on Foundry or claim the full retention, permission-race and product acceptance matrix is finished.

React bindings:

Wrap descendants in CanvasProvider with an existing engine and, for session hooks, an already attached viewId. The Provider borrows both; unmounting it does not close the project or detach a view owned by the host. Explicitly unmount the surface, detach its view, and close the engine at their respective lifetimes. Import from /react for React only, or the re-export in /react-flow.

  • useCanvas(selector, isEqual?) observes the one immutable project Root. Default equality is Object.is; choose an appropriate comparator for allocated results. Snapshots are cached per selection and unchanged selections do not notify React.
  • useElement(idOrTarget, selector?, isEqual?) returns present, absent, unloaded, or stale, or the supplied selector result. A Target includes incarnation; a restored element does not silently retarget it. Custom selectors use Object.is by default; provide a comparator for allocated field groups.
  • useActions(target) exposes stable rename, setData, resize, remove, and moveTo facades over normal editor commands. Optional command settings retain label/grouping/history semantics. Call actions from events, not during render.
  • useMode(target) returns the current view's mode and setMode; its session subscription is keyed by view and target. It never writes document/history state. Missing view context is an explicit error.
  • useGestureBindings(target) returns effective geometry, active ownership and begin/update/commit/cancel. A token protects the lease; cleanup cancels only its own lease. Controller replacements keep independent token slots. Preview geometry remains shared across views; active ownership and controls belong to the owning view. Host bindings still handle pointer capture and events.
  • useOperation(id) observes both the logical record and its frozen request; request-only changes are included without observing unrelated Root fields.
  • useAsset(id) resolves a releasable display handle, returns metadata plus loading/ready/error/url/retry, and releases on ref/owner change or unmount. Ready means the display handle resolved; it does not prove upload ACK, image decoding or a successful subsequent fetch of a remote fallback URL.
  • useSyncStatus(canvasId?) reports pending/blocked/unknown work plus loading, dirty and persistence errors. Durability is project-wide even when filtering operations to one canvas. It does not call an undurable Root synced. Pending counts logical ledger work; running generation/import job progress is observed separately from document synchronization.

engine.getDurability() now returns a stable readonly snapshot including closing, closed and an optional error. subscribeDurability observes successful cuts, failures and closure without creating another business Root. Hooks and the Provider own no second document, ledger, network queue or storage lifecycle. Source selectors still run on Root notifications; this is not a claim of complete per-field indexing.

A React renderer may use these hooks once its real engine exists. For the early read-only preview before an engine opens, declare a separate preview renderer in its ReactRendererDefinition. The assembly exposes previewNodeTypes and previewEdgeTypes. An omitted preview gets a passive preparing placeholder; the app never constructs a fake editor merely to satisfy the hooks. Preview functions must render the supplied records without requiring CanvasProvider. Built-in preview renderers are registered explicitly. Framework definitions never enter core data.

Presentation scheduling:

createFrameQueue(clock, publish) accumulates dirty keys in a Set and requests one frame. flush() publishes now; reentrant invalidations wait for the next frame. dispose() cancels pending work, including late canceled callbacks. This queue never holds commands, undo units or requests. createSessionFrameSource wraps a session source with a per-subscription queue; reads still return the current session immediately, while preview notifications coalesce per frame. The application uses it for CanvasView sessions. Pointerup reads its final event and commits through the engine synchronously; persistence and Root notifications are not frame-throttled. This does not implement paint extent indexing or selective per-element drawing: dirty keys currently schedule a view notification.

Node paint overflow:

createPaintIndex owns a rebuildable view-space grid of rectangles, with bounded cell expansion and oversized fallbacks. projectedPaintOverflow asks node geometry for the paint role separately from logical/hit geometry, transforms its bounds into scene space, and reports failed handlers conservatively. CanvasView queries the overflow index against the live viewport; visible overflow or unknown bounds suspend native culling so extended paint is retained. This currently disables culling for the view rather than selectively retaining one node. Geometry extensions must declare their paint extent; undeclared DOM overflow cannot be inferred. Edge/overlay paint indexing and region-based drawing remain separate work.

createProjectedPaintIndex retains the grid per engine/view and caches each node's paint result by projected node identity, immutable record identity and canvas transform. Unchanged nodes reuse their geometry result; removed nodes release cache entries. Viewport queries reuse the grid. clear() discards it. The cache still scans projected nodes and synchronizes rectangle membership; it is not a claim of constant-time full-frame updates or complete region-based drawing.

createJsonArrayImportSource is a seekable parser building block for a UTF-8 JSON array, with byte cursors, bounded record payloads, structural depth limits and abort/yield checks. It uses parserVersion 3 and can feed a host-managed generic ImportRunner. It does not yet interpret the exported document envelope, and ProjectEngine now accepts native record arrays through imports.startJsonArray(bytes, canvasId); the file picker routes .json to it. Exported document envelopes use the separate imports.startDocument path described below. A malformed structural tail ends with an explicit failure; previously emitted records remain available. Empty source bytes reject, while [] is a valid empty array. Array record errors are distinct from document schema/reference failures.

JSON array jobs preserve the original Blob and integrity digest, parserVersion 3, byte cursor, stable IDs, pending dependencies and failures using the same ImportJob and commit boundary as NDJSON. Such roots use recovery envelope 14; readers accept 1–16 and reject 17. Downgrading an array-bearing image to 13 rejects. This changes neither IndexedDB schema v4 nor the server wire protocol. Native array items have the same {kind,key,record} schema as NDJSON; $target denotes the chosen canvas.

imports.startDocument(bytes, canvasId) accepts the toolbar's exported document format v1. The JSON picker distinguishes object envelopes from record arrays. Source canvas ownership and references map to fresh IDs; its root maps to the explicit target, and tombstones are not copied into the target. The original file/digest and parserVersion persist. New jobs use parser 5: after the version, project and canvas headers, records are scanned and decoded on demand rather than indexing the full elements array first. The ordinal cursor resumes against the same bytes. Legal prefixes survive malformed array/footer tails with an explicit partial result. Headers following the elements array require indexed preparation to establish ownership first. Legacy parser 4 jobs keep their indexed interpreter. New document jobs use recovery v16; legacy document jobs keep v15 (readers accept 1–16, reject 17). That parser change retained DB v4 and the wire protocol; the subsequent checkpoint representation upgrade uses DB v5 separately. Resource-bearing records use fresh authorization when the host provides a depot and authority; missing capabilities or denied resources produce explicit partial results. Complete service acceptance remains pending. Source SHA verification still reads the original file before the runner starts; this removes the second full range scan, not the remaining integrity startup barrier.

ImportRunner hosts may provide async beforeCommit(units, job, signal) for read-only preparation such as resource permission checks. It returns Availability; a denial uses normal independent-group failure isolation. A successful result still must pass cancellation/hold/generation/status checks before the synchronous commit. No document, history or committed source progress advances while it waits. The document import host now uses this hook to authorize/adopt resource references as described below.

Document resource adoption now resolves fresh permission through the workspace AssetDepot before each candidate commit, verifies original bytes/digest, and canonicalizes assetId image URLs through the current authority. It retains at most 32 MiB of prepared bytes per group (a single in-progress authority read may add its separately limited 20 MiB); over-capacity groups fail explicitly. Authority or depot absence remains an explicit partial-record failure. Cancellation and generation fences run before the synchronous transaction. New bindings, adopted byte ownership, elements, history, ledger and job progress publish together; rejected transactions restore prior temporary bytes and leave no binding. Existing owned resources must match immutable stored metadata; no fake upload is created. Cached bytes never replace fresh permission resolution. Recovery remains v15.

projectedConnectionPaintOverflow adds conservative scene-space bounds for cubic control hulls plus half stroke. It uses the same geometry protocol and connector adapters as hit testing, with native handles for custom record renderers. Default work limits are 4096 edges and 4096 total cubic segments. Missing geometry, invalid values and exhausted budgets remain unknown and suspend native culling; they are not treated as empty paint. CanvasView queries a separate overflow grid against its viewport. This currently retains the whole view when needed; it is not selective edge mounting, exact curve bounds, overlay indexing or dirty-region rendering. Custom renderers must supply a compatible geometry protocol.

createOverlayIndex(viewId) owns ephemeral screen-space placements keyed by instance ID, each with a target incarnation, placement name, measured bounds, focus and composition state. Registration returns an update/release lease; a late old lease cannot remove its replacement. visible(rect, currentTarget) also retains focused/composing editors outside the rect and rejects stale targets. No overlay state enters the document, history, ledger or recovery image. The application registers frame settings and node style toolbars through an optional DOM context, including portalled content; legacy and pre-engine renderers use no registry. Other overlays need explicit registration. Current retention still suspends culling for the view; it does not selectively mount each overlay target.

Pointer transport helpers (ownsPointerCapture, acquirePointerCapture, releasePointerCapture) keep DOM capture separate from editor gesture ownership. Move/up/cancel must match the active pointer ID. Lost capture also matches its capture target and cannot cancel a target that already owns a newer platform capture. Capture acquisition failure cancels the acquired edit lease; detached release is idempotent. Node transforms and edge controls now share these checks and cancel/release on blur or unmount. Native React Flow drag handling remains an adapter path; these helpers do not replace the full tool/input controller system.

While a control owns an active capture, foreign pointer events reaching that control are consumed without changing the lease; they do not fall through into pane navigation/connection handlers. Outside an active control, unclaimed input continues normally. Node drag cancellation only addresses its own pointer/token.

The view projection cache now retains node/edge array identities when all retained items and their order are unchanged, so metadata-only roots do not hand React Flow new collections. A changed node replaces its node array while an unchanged edge array remains stable. Document and transform provenance still come from the fresh projection; lifecycle/scope changes and clear() invalidate the retained arrays. Projection calculation still scans the scene. This is not selective mounting or complete dirty-region rendering.

Additional node/overlay retention now checks whether the target is already kept by native rectangle visibility (including initial measurement/dragging). A toolbar on an already visible node no longer disables culling for every other node. Only an otherwise offscreen target that needs extra paint/overlay retention uses the view fallback. Focus/IME and gesture safeguards remain. This fixes a measured regression where selecting a frame mounted all 2003 nodes after a large copy.

Node paint signatures now compare effective geometry, lifecycle, owner, component, transform and declared geometry dataDependencies, rather than immutable record object identity. An ACK that reconstructs equal records no longer recomputes all node paint bounds. Unrelated titles do not invalidate a protocol that declares only another data field. Omitting dependencies conservatively compares all data; components must declare every data field their geometry reads. The scene still gets scanned, and connection paint currently has its separate computation path.

createConnectionPaintIndex retains curve hulls and grid membership across view updates. Its signature includes the connection record/projection, numeric endpoint anchors, transform and endpoint rectangles. Equal ACKs and unrelated node titles do not call curve/stroke geometry again. Cached segment counts still consume the query budget. Invalid geometry stays unknown and is retried; removed edges release entries. Registry/project/canvas changes or clear() reset the cache. Hosts whose geometry services capture changing external configuration must pass a changed geometryRevision to sync. CanvasView now uses this index with native handles. The edge list/signature comparison still gets scanned; this is not full dirty-region drawing or selective offscreen mounting.

React renderer assembly now memoizes installed node/edge/preview components using stable wrappers shared by original component identity. Explicit React.memo inputs are preserved. definitions[].render/preview retain the original implementations; nodeTypes/edgeTypes tables contain the compiled wrappers. Context and hook updates still propagate normally. The projection cache separately shares equal data/style/ position fields across measurement-only updates without mutating input objects. A real three-node fixture verifies that editing one node through ACK leaves an unrelated node and nonincident edge's render function counts unchanged. Connected edges may legitimately remeasure when a custom renderer's data changes; this does not assume arbitrary custom Handle layouts stay fixed.

Node projections now carry readonly __headlessTarget display metadata with id/incarnation/ownerCanvasId/display canvasId, written from model identity after user data is projected. It is not a persisted element field. The application binds legacy editing actions to that captured target through useScopedNodeEditing; old incarnation/owner/placement and detached/closed-view callbacks do not write to current records. Frame titles, node titles, text/style/branch/transform controls use the scoped bridge. Legacy non-headless contexts retain their old interface. Text grouping now records incarnation and drops obsolete groups. This is separate from the public explicit-Target action API and gesture lease tokens.

canvasKeyContext(nativeEvent) classifies text and overlay ownership using the composed path, including retargeted shadow inputs, native selects and registered data-canvas-overlay-view placements. Pass it to routeCanvasKey with the native IME state and the event's current defaultPrevented value. Window Escape fallback runs in the bubble phase so target handlers can claim it first; it cancels only this view's gesture. Style toolbars dismiss their own modes on unclaimed Escape, while input controls retain priority. The helper needs no browser globals at module load or when called with non-DOM ports.

Native import scanners share ImportScheduling (now, yieldTask, frameBudgetMs, default 6 ms). Array/envelope scanners check elapsed time every 128 scan steps; NDJSON checks after each bounded chunk. A 16 KiB/step work threshold also yields when the clock does not advance. Cancellation is checked again after each real task yield. Byte/ordinal cursors, parser versions and source fingerprints are unchanged. This is a soft budget: a bounded JSON decode is indivisible. Legacy and out-of-order document headers retain indexed preparation; canonical parser 5 headers allow progressive payload scanning.

The dedicated digest Worker uses native crypto.subtle.digest('SHA-256', ...) for inputs up to 64 MiB. The browser pool admits two active Workers, each reading at most 64 MiB on this path. This bounds admitted input per Worker, not total browser memory or native cleanup timing. Larger inputs or a missing native capability keep the 1 MiB streaming SHA implementation. Main-thread fallback stays chunked/cooperative. Native read/hash errors remain visible; they do not silently select another path. Worker termination still owns cancellation. The full source check remains before imported elements are published; no journal revisions or integrity gates are skipped.

Element hooks share one upstream editor listener per engine while subscribed. An ID→bucket Map and listener Sets route explicit changes to their targets; runtime-only cuts do no element lookup work. Document replacements check active IDs, and loading coverage changes recheck missing lookups. Reads still consult the current engine, including while unsubscribed. Buckets retain lookup values, not per-element Root snapshots; the last listener releases its bucket and the final bucket releases the upstream listener. No document, history or durable state is copied into the bindings. Arbitrary useCanvas selectors remain Root observers.

const title = useElement(target, view =>
  view.status === 'present' && typeof view.element.data.title === 'string'
    ? view.element.data.title : ''
);

A change to another field on this target evaluates the title selector but does not rerender its consumer. Unrelated target edits skip the selector entirely. This is ID routing and value selection, not a static field dependency index.

Mode and gesture hooks pair keyed element lookups with small session selections, instead of retaining a full Root/session pair per hook. All session channels in an engine share one upstream listener. Mode buckets use view→target Maps; gesture buckets use target IDs. Preview updates compare the active preview map, then notify only changed targets. A WeakMap indexes each immutable target array once for incarnation checks. Viewport/selection-only updates and connection previews do not reevaluate these node hook selectors.

The pairing caches lookup/selection tuples; values stay fresh on direct reads, and equal effective geometry/mode values suppress intermediate renders. The last subscription removes its bucket and releases the upstream listener. A controller identity change (engine/view/target/incarnation) creates a private lease slot, so old cleanup cannot consume the replacement controller's token. This does not change the one shared gesture owner, its history unit or durable task lifecycle.

Operation, asset metadata and sync-count channels share one upstream editor listener per engine. Their dependency tuples contain only the tables they read (and the loading flag for counts). Table identities use weak object IDs, so a channel caches a primitive version signature and selected values rather than an old Root or full table. Changed tables recheck subscribed IDs; unchanged tables skip those reads. Read caches and last-notified values stay separate so an early getSnapshot() cannot consume another observer's notification.

Sync counts build one scalar index per operations/requests/loading version: project totals plus canvas→counts. Requests spanning multiple pending operations count once per matching canvas. Operations with no canvas remain in project counts. Unknown canvas queries return the shared zero count without growing the index. This preserves the existing blocked/unknown definitions; table changes still require a traversal, not a per-record ledger write-set update.

Asset display effects keep their previous ownership, abort/release and retry rules. Durable-only completion/error/close is still read from the independent durability source, so saving and synchronization remain distinct.

The host's view-owned scene cache retains one current projection and its document, canvas, registry/catalog, selection, geometry preview, and connection target/path inputs. Viewport, tool, modes, task receipts and other views' selections do not rebuild that scene. Geometries are read from the captured session preview rather than a newer live session; shared previews still appear in every view. An empty node gesture and phase-only transitions do not require geometry projection. Field publications, selection changes and ordinary node/edge previews patch the changed projections through view-owned ID-to-array-position Maps. Continuous publication changes are coalesced in a dirty Set (up to 16384 IDs); missing change chains and canonical replacements compare document records and order first. Equivalent replacement records retain projected objects and transforms. Entry geometry/presentation, topology, membership/lifecycle, redirected endpoints and unsupported scenes retain full traversal. Canvas/renderer replacement also rebuilds. Array copying and existing paint/measurement/retention passes can still be linear; this is not a claim that the entire view is O(changed IDs). Numeric geometry caches remain derived Maps, with no history or durable task ownership. Engine/view replacement discards this cache; clear() also drops the retained scene and numeric caches. Query provenance is validated even on a cache hit.

Session transitions publish the final view and any owned preview cancellation in one snapshot. Changing tools cancels only that view's active node/path gesture; begin/beginConnection require the selection tool (tool_not_select otherwise). Repeated selection of the current tool is a no-op. The React Flow adapter also disables native connection handles in pan mode and rejects a late connection callback against the current tool. Multitouch interruption is view scoped.

Modes remain Map<ElementId, string> in each view. On document publication, the session compares the previous and current target incarnation, owner, structure, component and data version: replacing or moving the target clears its mode, while content edits and equivalent canonical clones preserve it. Missing targets cannot acquire a mode. Identical mode updates and redundant clears do not notify. This uses a borrowed current document reference, not a second mode identity store.

Each attach disposer runs once, so a late second cleanup cannot detach a newly attached view with the same ID. Explicit canvas navigation and detach publish the new view set together with cancellation. Session disposal fences admission before notifying, clears all views/previews, and is idempotent. These transient actions create no document/history/ledger entries and change no recovery format.

Gesture admission also checks the target owner's visible ancestry relative to the owning view's canvas. A collapsed or opaque ancestor is target_hidden; an owner outside that view's subtree is target_outside_view. Node and path gestures use the same rule. The temporary owner-to-chain Map shares work for multi-selection; it lives only during admission or a document publication, with no project scan.

An active gesture cancels with coordinate_context_changed when an entry in that relative chain changes geometry, normalized content/unit scale, identity, or parent relation, or becomes hidden. Entry titles and unrelated style fields do not affect the chain. Ancestors above an independently opened child view are excluded. Target component/version changes cancel with target_changed; removal of the view canvas publishes fallback navigation and canvas_changed together. Late updates/commit cannot use the canceled preview. Failed admission does not replace the accepted baseline, and no chain, preview or gesture enters recovery.

Internal parser byte ports and read evidence (import-byte-reader.ts, import-read-proof.ts) support progressive native imports before whole-file hashing completes. NDJSON, JSON array, streaming document and its indexed fallback all accept the optional readBytes port; the default Blob path keeps its original slice request bounds and verifies the returned byte count.

The proof reader binds a file ID and byte length to SHA-256 hashes of aligned 16 KiB blocks, recording evidence only after an entire range is delivered. A restored manifest can verify its recorded blocks before parsing resumes. Unrecorded blocks are explicitly outside this proof: the manifest is neither a whole-file digest nor authority to dispatch mutations. New native jobs use source integrity version 2 and recovery format 17. Parser cursor, IDs, pending locators, read evidence and accepted document changes publish through the same StateRoot. The complete-source digest runs concurrently; content can display and be edited before that digest finishes. Legacy source version 1 keeps its SHA-first path, including array recovery 14 and streaming document recovery 16. Copy sources retain their existing interpreters. IndexedDB schema remains 4; wire remains 2.

The scheduler admits no newly compiled project mutation while any accepted source is unsealed or its seal has not reached storage. durableImportSeals is a derived file-ID → SHA Map rebuilt from each successfully stored cut, not a permission boolean or a second job store. Existing immutable requests keep their original verified bodies. The gate is conservatively project-wide, including unrelated new edits; dependency-scoped dispatch is not claimed.

Pause, partial success and cancel after an accepted prefix retain verification and source pins. Cancel before any accepted content abandons verification without blocking unrelated sends. Read/digest failure retains local content and the gate; retry() rebuilds the reader and verifies again without restarting a cancelled parser. Reopening verifies saved block evidence before continuing the same cursor and IDs. Known complete hashes are checked before resumed parsing. A late SHA seal changes source metadata without changing parser generation; every parser checkpoint merges that current engine-owned metadata instead of reverting it. Public runtime updates cannot forge or remove existing proof/hash evidence.

Readers and verification tasks belong to ProjectEngine, with per-job Maps and AbortControllers. Recovery holds abort and later restart unfinished verification; close fences late results and releases readers. Job source metadata is durable; read caches, pending Promises and controllers are transient. No extra document, business queue or history stack is introduced.

The reader owns a hash Map (at most 4096 blocks for the 64 MiB source limit), a bounded LRU byte Map (four blocks by default), and up to four shared in-flight block Promises/four range reads. Each range is capped at 4 MiB. Aborted waiters release their own read but do not free occupied native reads early or cancel another waiter. Close fences late results and clears byte buffers; checkpoints contain no byte buffers. These bounds do not assert native Blob memory release.

Byte I/O/capacity/integrity failures throw instead of becoming a terminal malformed record. In particular a document reader preserves its ordinal for retry; the engine pauses the import, retains its editable prefix, and resumes after the I/O recovers. Ordinary malformed JSON/UTF-8 still uses the existing record diagnostics.

Ordinary node insertion now keeps existing node/edge projections and transforms. A continuous change hint records element IDs rather than treating every element creation as a canvas topology change. A new visible known node uses the shared single-node builder; ordered member IDs restore the original depth-first canvas order. Hidden additions update the document without rebuilding visible objects. The layout index is rebuilt when visible node membership changes, so subsequent field edits address the new indices. Canvas changes, removal, reparenting, connection insertion/endpoints and unsupported placeholders retain full traversal.

This reduces projection construction, not the whole path to O(k). Reordering node references and rebuilding layout indices still takes linear work; missing membership services require a record scan. The 1000-node insertion test resolves only the new renderer, keeps all 1000 prior node objects and forbids full document record enumeration when a membership index is available. Actual toolbar insertion and ACK are checked separately in the page integration harness.

A local Chrome157/Next-development 1000-child copy probe completed with a first presented frame around 348 ms and frame-gap P95 around 33 ms. These observations are not an SLA or proof of overall speedup. The 10000-child baseline probe at 0ce3abb remained unresponsive with a CPU-active renderer and approximately 4.4 GiB sampled physical footprint, and was interrupted. Its completion, first frame and root cause remain unverified; 10000-node support is not claimed.

Node measurement requests now share a microtask batch per React Flow store. createNodeInternalsBatch owns a Map from mount tokens to node IDs and a dirty Set of tokens. A flush deduplicates live IDs and invokes the public bulk update once. Releasing a mount removes its pending request without touching a replacement mount for the same ID. Reentrant invalidation schedules another flush; separate stores never share work. The React adapter keys its WeakMap by getState, because useStoreApi() creates a separate wrapper per hook. Acquisition/release happens in effects, and the invalidate callback stays stable across renders. The SDK still schedules its own animation frame; the adapter adds no extra frame timer.

Inactive node/frame toolbars no longer mount NodeToolbar. AnimatePresence owns the conditional toolbar so selection, portal editing and exit animation remain available. In React Flow 12.12.1 each mounted toolbar subscribes to a selector that scans the whole node array; an empty child was not an inactive subscription. Likewise each individual forced measurement updates all absolute positions, so per-node mount calls multiplied work. Both paths are bounded by actual active UI needs now. These are renderer resources; no model, history or durable queue state changes.

A 10000-node page reached its initial scene after these changes, but its copy probe still exceeded the 240-second completion window. During the copy it accepted read-only diagnostics and displayed incremental content; commit/persistence costs remain unresolved at that scale. Do not interpret the initial-scene improvement or the batching checks as a passed 10000-node copy budget.

Browser storage writes journalVersion: 2 deltas whose SHA-256 covers the ordered JSON object {journalVersion, baseRevision, baseChecksum, header, patches}. The prior checksum may belong to a full checkpoint, a v1 delta's reconstructed image, or another v2 delta. Checkpoints and full-record fallbacks still hash the entire image. Readers retain v1's full-image checksum rule and select the interpreter explicitly; unknown journal versions fail. DB schema remains 4 and RecoveryImage remains 1–17. This changes the internal journal interpreter, not wire protocol 2.

The recovery codec records exact UTF-8 byte lengths while producing immutable JSON, including omission/null normalization and escaped Unicode. Only factory-owned images expose the cached size, so choosing a smaller delta no longer first serializes the complete owned image. Mutable callers retain defensive copying and full size calculation. Noncanonical image header order/fields use a full record to preserve exact serialization.

Each leader retains its last verified image/checksum and checkpoint revision in WeakMaps. Both advance only after transaction completion. Every next cut still checks stored metadata, prior checksum, continuous revision and local revision; all file/depot writes remain atomic with the cut. Same-cut retries reread and validate the chain, compare complete serialized images (including property order), and keep the existing restrictions on new files. No Root cuts are skipped. Recovery hashes/applies outside the read transaction, validates every reconstructed image, and retains original frozen request bytes. A v1-only reader must reject a v2 row rather than ignore its checksum semantics.

The 10000-child copy probe still exceeded its 240-second completion window after this change. The exact hashed-input browser test proves removal of whole-image hashing for ordinary adjacent delta writes; this is not a passed end-to-end copy budget or a claim that encoding, rendering and dispatch costs are all resolved.

Import identity capture now reuses a frozen, own-data string dictionary when no new ID is reserved. One active runner retains its last ID-table reference and a reverse Set; only the first actual allocation copies both. Mutable dictionaries and frozen getters are captured defensively, and a late reserve callback cannot mutate an already published result. The index is cleared when the pump ends, including pause/cancel/close/restart; the durable stableIds remains on ImportJob. Allocation keeps the caller port method receiver. No task or recovery version changes, and first-item publication/durability barriers remain in place.

Recovery also reuses frozen plain document views across metadata-only Roots. isFactoryDocument identifies an envelope with fixed table membership produced by core constructors; it is not a content-validation or deep-immutability test. A WeakMap owns only a derived plain view. Its array/tuple containers are frozen without freezing caller-owned records. The codec still independently checks deep immutability, so mutable values inherited through applyChanges are reevaluated. Unowned envelopes reread mutable map membership. A test prohibits document-record enumeration during a metadata-only encode after the initial view is built.

The 1000-child copy probe completes with these paths, but the 10000-child copy still exceeds 240 seconds. Removing ID-table copies and metadata-only document scans is directly verified; a general throughput improvement at 10000 nodes is not established. Per-record commit, ACK and remaining table traversal costs remain part of the larger implementation goal.

Import commit sizing uses runner-local feedback rather than dividing total transaction time by unit count and treating it as a purely marginal cost. The first ready record remains immediate. Successful batches within the soft frame budget grow by a bounded step; an over-budget batch reduces the limit. A short tail or dependency group cannot increase it. maxBatch, byte/change limits, atomic reference cycles, parser slices and durability backpressure keep their existing roles. Rejected attempts do not train the success-cost estimate. The limit is a scheduling hint, not another persistent queue or an assurance that an indivisible transaction or subsequent renderer work fits one frame.

Node paint entries retain a detached dependency signature until an input changes. Unchanged rows compare scalar geometry/transform/preview values and declared data fields directly, without allocating replacement signature objects or dependency arrays. Unspecified data dependencies still compare the complete data snapshot. Nested mutable data is copied on capture, and renderer fallback changes the selected protocol, so neither same object identity nor equal geometry alone permits reuse. stats().captured counts actual dependency captures. Projection retention also keeps the existing retained row when the incoming node is the same object in the same lifetime. Both caches remain derived view-owned state; the full membership scan and paint-grid reconciliation remain present.

Editor create commands now stage transaction-local append views instead of copying all document tables for every element/canvas. Each view has fixed membership, so a policy or caller retaining an earlier getDocument() prefix cannot observe later additions. Ordered policy checks, ID/tombstone admission, record cloning and final document validation remain unchanged. At most 64 new entries are copied in a staging window before folding to a new read-only base. The transaction materializes an owned document once before publication; a non-create command can materialize earlier through the normal change path. These views are unpublished preparation, not a second document store or durable queue. Rollback drops them without publishing history, ledger or runtime state.

Resource retention now shares one synchronous history/ledger analysis between working and acknowledged document scans. The temporary scanner holds interpreted transition ranges, the earliest pending sequence, the resource-field union and schema candidates only for IDs used by historical/pending data-field changes. Create-only histories need no per-element schema table. Both documents still produce their own pins and completeness result; all supported transitions, including resolved evidence, are validated. Cross-schema fallback pins and owner order are preserved. The scanner is internal to one retention pass and is not stored across Roots, projects or registry changes.

Query view construction no longer leaves a callback capturing the previous query in the retained method context. Latest methods retain their own document/tables, not the complete chain of discarded generations. Explicitly held historical queries remain valid. A child-process GC check covers raw queries and the actual editor with query coverage installed.

Persistence uses one writer and the existing pending Root Map. writePrefix captures a finite Root boundary, validates every receipt and publishes at most one durable-generation update for that prefix. The receipt-derived maximum survives a failed metadata publication as a writer publication obligation. The writer promise is installed before work, so synchronous listeners cannot start another writer. No existing Root is skipped or rewritten.

Transport's internal persistThrough waits for its fixed request/dispatch Root; it does not chase later local edits. Public persist still flushes all pending Roots and the derived publication obligation, including metadata echoes. Failed writes reject their barriers and retain the exact suffix for retry. Network capacity grants recheck leader/identity before calling the adapter. A fenced scheduler pump reports failed state publication without an unhandled rejection. settle repeats until the Root and current task queues stabilize, preserving future retry/held-job semantics instead of relying on two fixed passes.

The actual-page test uses bounded CDP requests and rejects disconnected/crashed or dialog-blocked targets without accepting dialogs. A failed protocol call cannot hang a second metrics read. Browser cleanup escalates only the fixture's own process after its exit grace period. Optional heap/receipt sampling and a pinned baseline override affect only fixture copies and are diagnostic, not product APIs or production performance claims.

Recovery serialization memoizes entry arrays only for core-owned read-only Map views. The core tracks membership ownership in a WeakSet; the codec's WeakMap holds frozen pair/array containers per view. Value objects are not frozen by this step. The existing recursive codec still checks nested mutability and depth, so mutable values and external Maps are re-read. This is an encoding cache, not a second state table; recovery JSON order, formats and journal checks remain the same. Replacing a table gets a new entry view and leaves prior images unchanged.

Static element validation proofs apply only to records detached and deeply frozen by the core's element-data factory. Document construction, element replacement and staged creations share this private provenance. Re-enveloping an already owned record preserves its immutable value identity instead of cloning it again; external and lookalike inputs are still detached. A separate WeakSet records a successful intrinsic shape check. Registry support, tombstones, owner membership and connection topology remain live checks. Custom schemas are always called with their original receiver. Only framework-owned data-only validators can reuse a successful result for the same detached record and current validator function; their private proof keeps one validator identity per record. External or shallow-frozen records never gain a proof. Field updates without factory provenance use the complete path. Document equality rejects different table sizes before allocating entry arrays; equal-sized tables keep the existing ordered comparison and no-op rules.

Synchronous document validation loops directly over the same per-record checks used by the cooperative iterator. The iterator retains its original yield points; the atomic publication path does not pay a generator handoff per unchanged record.

Resolved Foundry document receipts have an explicit retention policy. The SDK keeps raw receipts by default; CanvasRuntimeHost opts into summary. Only a successfully reconciled protocol-2 document change with known model/record fields can become the adapter's foundry.reconciled-document version-1 summary. It keeps the request ID, covered logical operation IDs, project ID and actual returned canvas versions. Frozen request bytes, ledger outcome, ACK base and history remain in their existing owners. This summary does not retain the complete raw response for audit, and cannot be used as a fresh server proof or replay input.

The engine prepares the summary for the same Root publication that resolves the ledger and installs the ACK/document/history result. If that publication rejects, the original raw receipt remains available with the unresolved request. A failed or invalid summary codec reports the error and retains raw data without failing a valid ACK. Unknown fields/components/versions, unresolved requests and run/asset receipts stay raw. Existing stored raw receipts are not migrated by this policy. The recovery envelope remains unchanged: receipts are opaque JSON, and the adapter-local summary has its own version and explicit replay rejection.

The application's initial loading fit targets the current canvas's direct node members, including partition entries. Expanded descendant cards remain rendered and pannable but cannot move their parent entry outside the initial viewport. The manual fit control keeps its existing whole-scene behavior.

Pending-operation reconciliation replays ordered change batches in one private core draft. Each change still sees the results of earlier changes: field presence, incarnation, before-value conditions and current owner scope are checked in that order. Already-satisfied changes are skipped, but field scope is checked first. Only the final document envelope escapes; a late conflict discards the draft and leaves the input ACK and ledger unchanged. The runtime retains sequence sorting, operation/history-effect admission and final registry/topology validation. This removes a full table copy for every pending change without adding intermediate Roots, history units or network writes. applyChanges uses the same conditional change application path and retains its existing strict/default behavior.

CanvasView coalesces Root presentation notifications with createFrameSource. Local user updates/deletes, undo and redo flush pending presentation work synchronously so controlled inputs retain their native value/caret. Pure creation and background invalidations may share a frame. Each subscription owns one disposable frame queue; all editor commits still advance scene-change hints synchronously. Snapshot reads always delegate to the current authoritative Root, so commands, history, persistence and input identity checks do not wait for a frame. The adapter holds no delayed document snapshot. Unsubscription cancels pending work, including callbacks delivered late by a clock; eager subscription failures also dispose their queue. This batches source notifications, not every React render: another store/prop update may read the current Root before the pending frame. Session and element subscriptions retain their existing routing and immediate read semantics.

The actual-page performance harness defaults to development webpack. Add --production together with --performance to build its isolated application with next build --webpack and serve that completed build with next start. The report identifies the mode and records build time separately from browser copy timings; build failure aborts before measurement. Example:

HEADLESS_PERFORMANCE_COUNT=10000 HEADLESS_PERFORMANCE_REPORT=/private/tmp/headless-production-10000.json node tests/headless/renderer.integration.mjs --performance --production

Both modes retain the same full-copy, upper-canvas edge, synchronization and far-card viewport assertions. Production here means an optimized frontend served against the fixture HTTP service; it does not claim production Foundry validation.

Copy snapshot encoding/index scanning and legacy prepared-source encoding share the import time-slice scheduler through an internal copy-budget adapter. They no longer sleep every 32 records. Record bytes contribute weighted work so larger records trigger budget checks sooner; the existing coarse-clock work ceiling is retained. Topology/full validation iterators use the same slice budget. This is a soft scheduling budget: JSON operations, document construction and a single validation step remain indivisible. Copy cancellation still reports copy_aborted; host clock/yield errors remain observable. Snapshot formats, byte locators, size limits, source digests, lazy payload reads and admission/seal ordering are unchanged. No partially encoded snapshot is admitted or returned.

ProjectEngine allows the existing import feedback controller to probe batches up to 128 ordinary units; the low-level runner's default remains 32 unless the host overrides it. The ceiling is not a fixed batch size or a higher time budget. Initial batches start at one; measured commits still grow/back off against the same 6ms soft budget. Parser time checks, the pending/decoded byte limits, SCC isolation, resource authorization limits, durable send barriers and source ordering remain in force. Hosts that set a lower runner ceiling retain that ordinary-batch bound. An indivisible SCC can exceed the soft unit limit under its existing hard byte and change-count limits.

Full browser recovery cuts now use a versioned envelope: {checkpointVersion:1, header:{formatVersion,rootRevision,localRevision}, bytes:Blob, checksum}. The Blob contains exact UTF-8 JSON.stringify(image) bytes, not another document model. It can represent a checkpoint or a full replacement journal cut. Delta journals retain versions 1/2; legacy object cuts remain readable. DB v5 fences DB v4 clients before these rows are written. Recovery envelope/wire versions do not change. Rolling application code back requires retaining the v5 storage reader; an old v4 client cannot open the upgraded database.

For codec-owned sealed images, serialization splits large containers/strings and uses existing codec byte facts to stringify small owned subtrees natively. It preserves key order, escaping and surrogate pairs, yielding on the shared soft time/work budget before starting an IDB write transaction. External images retain the defensive capture/native JSON path. Each yield rechecks the storage owner. The complete Blob and checksum are prepared first; head, cut, files and depot changes still commit in one transaction. No Root is skipped. Reopening validates headers/sequence, hashes exact Blob bytes, decodes strict UTF-8, checks the decoded header and then applies later deltas. Future checkpoint versions reject reads and writes; malformed/tampered data is never treated as an empty document. Legacy recovery, JSON parse, recovery-image capture, same-cut retry comparison and individual native operations still have synchronous work; this is not a strict 6ms bound for all storage paths or a disk-space reduction guarantee.

Before Blob serialization, ProjectEngine now awaits cooperative recovery-image encoding inside its existing ordered Root writer. Private dataArray / dataRecord constructors copy and freeze DTO container membership; they do not claim their children are immutable. Preparation captures each unknown subtree synchronously, including getters, custom toJSON, Dates and external shallow-frozen records. Previously validated immutable codec facts can be reused. Only then does a budgeted traversal assemble the owned containers, preserving key order, omitted values, sparse-array normalization, depth checks and exact UTF-8 byte facts. The default uses scheduler.yield() when available (with its receiver), otherwise a timer, sharing the 6ms soft budget / 16384-work ceiling. One shared budget spans nested containers. Public synchronous encoding and recovery/storage formats stay unchanged; async images receive the same codec ownership seal without a second full encoding pass.

A yield does not publish or skip a Root. Later edits may publish while encoding is suspended; the old image still contains its captured state, and the next cut is saved after it. Request dispatch retains the durable-prefix barrier. Encoding or scheduling failures retain the pending Root for retry and produce no partial storage commit. Initial DTO membership copying, synchronous preparation, unknown subtrees and individual scalar operations remain synchronous. First-time JSON normalization can defer only when its detached provenance is established. This is partial time slicing, not a strict whole-Root latency bound. Grouped history entries now keep shape ownership for newly built changes and operation-ID arrays, including undo/redo lifecycle remaps. Import runners keep it for their own job envelopes, completed-key arrays, binding dictionaries and pending locator tables; newly allocated identity tables use the same private constructor. The engine's source proof refresh preserves this envelope ownership. Arbitrary host checkpoint replacements keep defensive capture. These constructors still freeze nested data before freezing their container, retaining the prior deep-freeze behavior; their brand alone never permits mutable child values to escape capture.

Primitive values captured in these tables cannot change while suspended. Their normalization and UTF-8 sizing now run in the cooperative traversal rather than the preparation scan. Unknown object/proxy reflection stays entirely within the original normalizer; preparation adds no proxy traps or callback invocations. Membership copying and the preparation scan itself remain synchronous, as do unknown subtrees and individual native operations.

A blocked schema upgrade retires its pending open. If the old connection closes later, that abandoned request aborts in onupgradeneeded rather than silently upgrading after rejection. A fresh adapter can retry once the blocking client has closed; closing an adapter before its upgrade event also aborts the pending open.

Stream-copy planning separates cooperative task yields from durable preparation checkpoints. ProjectEngine requests checkpoints on phase changes, the first nonempty ID allocation, approximately 50ms of elapsed work or 8192 planning steps at slice boundaries. It also persists the final ready mapping before entering execution. The planner exposes only a tiny immutable progress read to make this decision; full checkpoint cloning happens only when saving/returning. Persistence wait time is excluded from the next interval. Each small planning slice still yields and checks cancellation. Already-published Roots retain their normal ordered durability; unsaved work is private preparation with no created elements or requests. The low-level helper defaults to its prior per-slice checkpoint policy unless a cadence is requested. Checkpoint/source formats and resume IDs stay unchanged; a crash/pause can redo only unsaved private planning.

The app renderer retains explicit view owners without turning off native culling. Adapter-local __endoleapRetain flags cover node modes/gestures, focused native inputs, registered Portal overlays, known node paint overflow and curved-edge paint. Retained connections also retain their endpoint owners. The complete nodes/edges stay in React Flow's lookup; geometry, dragging, model data and hidden state are not fabricated. A view-local weak cache preserves tagged row identity, and ending an owner returns the original row. Detached focus is retired even when removing a renderer emits no blur. Flags never enter a Root or file. Application panels/toolbars explicitly mark data-canvas-ui-view with their current view ID, including portalled dialogs. Their inputs have a lifetime independent of node culling; focusing them does not pin native nodes. Registered node portals still declare their explicit target. Only unscoped third-party inputs/portals keep the conservative whole-graph compatibility fallback; missing ownership is not guessed. Exhausting curve-inspection work retains the edge layer and its endpoints, while unrelated nodes remain culled. This can still retain many endpoints/edges; it is not a constant-cost or complete geometry proof.

React Flow 12.12.1 does not expose a retained-ID selector. The app-only scripts/loaders/xyflow-retained.cjs extends its two ESM visibility selectors and fixes the empty selection-listener guard at compile time, under both webpack and Turbopack in next.config.ts. It requires the pinned version and exact source anchors, rejecting mismatches before silently losing editor retention. Dependency upgrades require reviewing this extension. Shared node_modules are not edited. This is an app renderer build requirement; the portable headless SDK/optional bindings do not include the Next loader.

The pinned dependency treats an empty onSelectionChangeHandlers array as truthy. The app guard checks its length, so an unused SelectionListenerInner no longer scans both native lookups on every store update. A callback prop or a real useOnSelectionChange subscriber still mounts the original listener. This does not add a second selection cache. The isolated page fixture checks notification, unsubscribe/re-subscribe and scan counts; counters and the fixture observer are excluded from performance builds and the production app. The VM regression also checks callback-prop delivery against both installed ESM entry files.

The actual-page performance probe double-clicks a visible text card with native Chrome input, checks mounted-node growth, inserts text and checks DOM identity, focus and content before measuring deep copy. It then clears selection with a native empty-pane click. Reports include inputCheck.beforeEditing and inputCheck.duringEditing, inputCheck.offscreenEditing and inputCheck.offscreenPortal; this preflight is outside the copy timing window. The screen-pan probes preserve native IME and an unsaved Portal title, bound mounted owners, check release after blur, and remount saved content. Independent AI-panel focus/pan is recorded as panelFocusedNodes/offscreenPanel; blur saves settings without starting a generation task. The actual-page suite also exceeds the 4096-edge inspection limit and checks unrelated offscreen cards stay culled. Add --turbopack to the page/performance runner to use the app's default compiler. Turbopack fixtures live temporarily under the physical dependency workspace so its filesystem fence can resolve Next; the exact unique directory is removed in finally. Webpack fixtures continue to use the OS temporary directory.

Set HEADLESS_PERFORMANCE_EDIT_DURING_COPY=1 with --performance and at least 1000 nodes to add a mixed-workload probe. After the first copied frame, it finds an exposed native hit point on the original card, double-clicks it, composes Chinese text and inserts three text chunks while copying. It checks focus/DOM retention, bounded mounted nodes and that input actually overlapped publication. After all ACKs it checks the original's new text and the copy's captured old text separately. No synthetic click passes through an overlapping copied card. duringCopyInput.samples records browser input-event time to the second rAF, latest value and retained focus. These frame opportunities exclude time before the browser dispatches the input event; they are not end-to-end key latency or proof that every intermediate composition string reached the display. Mixed workload timings must be kept separate from the no-input copy baseline.

Resource retention reuses flattened history-transition changes across durability cuts only after successful validation and a descriptor-based proof of immutable plain data. Proofs are weakly held. Every own property must be a data property; shallow freezing, accessors, custom prototypes, sparse arrays and hidden cycles do not qualify. Cached ranges are frozen. Mutable evidence remains live and is validated on every read. This cache contains neither resource IDs nor schema results: each scan still applies the current registry and keeps working/ACK owners separate. Explicit validateHistoryTransition(..., checkChanges) calls always execute the caller's validator. No persistence/wire format changes.

CPU/allocation diagnostics found repeated history-transition validation under resource retention. The production mixed-input probe and a same-machine old history-path control both pass, but observed total runtimes are similar; this change demonstrates reduced repeated interpretation, not an end-to-end speedup.

The 10k production copy/input probe keeps one native editor focused from the first presented copy through the 50% and 90% publication thresholds, read from the actual import status UI. Samples record phase, committed count, focus/DOM retention and input-to-two-rAF delay; middle/tail samples also record CDP dispatch round-trip time separately. That round trip includes transport and driver work, and is not a precise browser queue-time measurement. The final server document must retain the original's later edits while the copy retains its earlier source snapshot. Smaller scenarios retain the early burst because they can finish before a later publication threshold is observed.

Pending import records remain bounded seekable locators, not decoded document payloads. Runner-created locators, dependency arrays, parser appends and filtered pending tables retain private shape ownership, so recovery encoding can slice these waiting records even when endpoints have not arrived. Dependency admission, capacity checks and durable send barriers are unchanged. The asynchronous encoder also rechecks its immutable fact cache when assembling a prepared container: when the same source occurs twice in one image, the second occurrence can reuse the first completed encoding. Preparation still captures unsafe occurrences independently; getters and mutable descendants never qualify for this reuse.

First-time element and planning-checkpoint recovery encoding uses the core's detached-JSON ownership proof instead of synchronously normalizing every field before the first yield. Only detachJsonData results qualify: JSON-cloned, recursively frozen records with immutable JSON descendants. A shallow freeze, a document factory envelope, or an arbitrary host object does not qualify. Preparation requires the standard Object/Array prototype chain without inherited toJSON; otherwise it captures through the original normalizer. Every unknown normalization invalidates this prototype check because callbacks may change the environment. The cooperative walk reads the already detached own data and validates depth while building the same immutable JSON and byte facts. A callback later in capture cannot change an earlier record's meaning. Cold and cached subtrees both retain the depth limit. This moves the first full field/row walk into the shared budget; container-member capture and individual native/scalar operations still have synchronous work.

Step 4's acceptance evidence, commands and checkpoint encoding boundaries are recorded in STEP4_ACCEPTANCE.md. Passing functional fixtures is not a claim that all performance limits are resolved.

The shared core json-data.ts factory gives element data and duplicate planner checkpoints the same unforgeable clone provenance; element validity and schema proofs remain separate. It replaces their existing freeze(clone(...)) operation without a second clone or a wire-format change. Engine-created job/source/ preparation envelopes keep shape ownership through admission, checkpoint refresh, ready mapping, source verification and pause/resume/cancel. Fresh allocation arrays, ready ID tables and root lists use the same shape constructors. Unknown children still require synchronous capture; copying an envelope never grants a deep-data fact to them. Legacy/restored data retains defensive capture until a new planner snapshot supplies fresh provenance.

A final ready plan has already crossed its required preparation durability barrier. Its encoded facts may therefore already exist, and the ready source can reuse them without an artificial yield. Tests distinguish first-time preparation slicing from ready-source byte/mapping preservation and reopening.

AI wire negotiation

The AI service adapter explicitly requests responseFormat: 'canvas-v2' for admission, cancellation, output application, receipt queries, model catalogs and run polling. createFoundryTransport maps it to Accept: application/vnd.foundry.canvas+json;version=2. A custom ServiceTransport must preserve that negotiation as well as the frozen body, idempotency key, conditional version and account scope. The Next application proxy also declares v2, but an independent SDK consumer cannot rely on that proxy being present.

runs.test.mjs exercises all three writes and their receipt queries through the shared HTTP transport. foundry-ai.integration.mjs additionally requires a real successful 202 before testing lost admission recovery; a rejected admission is not evidence for response-loss recovery. Its remote worker fixture replaces only generation output, not authenticated HTTP, PostgreSQL or service receipts.

createFoundryAssets accepts a host displayURL(id) callback. Its default is Endoleap's same-origin /api/foundry/... preview route; a Node or other host must provide its own reachable preview route and arrange authentication there. This does not change sourceURL, canonical asset identity or the authority scope. Use assets.authority.read for original bytes and digest verification: a display preview may have a different encoding or size. Credentials remain in the host's fetch/transport, never in document records or generated display URLs.

Pinned real-service results, coverage and remaining audit boundaries are recorded in SERVICE_ACCEPTANCE.md.

The complete T01–T36 ownership, direct assertions, recovery cuts and current delivery boundaries are recorded in FINAL_ACCEPTANCE.md.