#UniCAS Architecture
Status: implemented service architecture; compatibility and retention tooling continue to evolve
Date: 2026-08-21
This document defines the content-addressed storage architecture implemented by UniCAS. Downstream applications may store their own opaque document and asset formats without making those formats part of the service contract.
The node encoding referenced below is defined in CAS Binary Format.
The public App/Space v1 contract, current D1 schema, and R2 object keys use App and Space dimensions. Historical binding, Durable Object class, audit-port, and audit-record names remain isolated implementation details rather than public aliases.
#1. Goals
The CAS stores an App-and-Space-scoped Merkle DAG for persistent SValue document roots and binary assets.
The design must provide:
- immutable, content-addressed nodes;
- App-and-Space isolation and authenticated service access;
- deduplication within one App-and-Space partition;
- child references between nodes;
- leases that protect uncommitted nodes from garbage collection;
- separate child and business-root reference counts;
- transactional reference updates inside D1;
- idempotent root-reference updates;
- deterministic reference extraction from document operations and snapshots;
- read-only CAS access from document-type implementations.
The CAS does not attempt to provide a distributed transaction spanning a document Durable Object, D1, and R2. Cross-system failures are handled with ordering, rollback, retry, and business-level compensation.
#2. App and Space isolation
App.status = suspended is a reversible App-wide data-plane deny override.
Issuer resolution includes the owning App's status for external and managed
issuers, and the Space verifier denies every operation with APP_SUSPENDED.
Cached authority refreshes after 30 seconds and cannot authorize beyond a
60-second hard stale bound on refresh failure. Administrator recovery routes
remain available. Suspension does not change retention, membership, or data;
see operations.
appId is the top-level trust and storage namespace. Within an App,
spaceId identifies data ownership. Root Ref revisions order events across
Spaces within the App, without an additional namespace.
- Every public data-plane route carries both
appIdandspaceId. - Identical content in different App-and-Space partitions is stored independently.
- Storage usage and GC are calculated per App-and-Space partition.
- App administrators can read a derived App total through the control plane; this aggregation does not expose Space identity or grant data-plane access.
- A configured trusted JWT issuer maps to one stable App; verified Space claims, permissions, and path Space must agree before storage access.
The current Cloudflare physical compatibility mapping is:
- D1 keys include
(stack_id, tenant_id, digest). - R2 objects use
stacks/{stackId}/tenants/{tenantId}/nodes/{digest}.
A CAS Durable Object named from the physical composite corresponding to
(appId, spaceId) coordinates mutable Space operations through a short
explicit mutation gate:
- lease claims and extensions;
- upload completion;
- child-reference creation;
- root-reference count updates;
- garbage collection.
D1 and R2 remain the durable stores. Canonical request bodies stream to R2 outside the mutation gate; only lease begin/finalize, Root Ref updates, and GC mutations queue. Active upload reservations fence GC during that unlocked interval. Content, metadata, and usage reads do not enter the mutation gate. The Durable Object does not replace App/Space-aware logical keys or their current physical compatibility keys in the shared D1 and R2 bindings.
#3. Node model
A logical node consists of three storage classes.
#3.1 Immutable content
The node's own content bytes are immutable and stored in R2.
stacks/{stackId}/tenants/{tenantId}/nodes/{sha256Digest}
This is the current physical compatibility key, not the v2 public route. R2 stores only the content bytes, not mutable lifecycle state.
#3.2 Immutable metadata
D1 stores metadata that participates in node identity:
export type CasHash = string; // 64 lowercase hexadecimal SHA-256 characters
export interface CasNodeMetadata {
readonly hash: CasHash;
/** Byte length of this node's own R2 content. */
readonly size: number;
readonly contentType: string;
/** Ordered child references. Duplicates are significant. */
readonly refs: readonly CasHash[];
}
The ordered refs list is part of the Merkle identity. Each occurrence contributes one child reference. D1 must retain ordering, even if it also maintains a grouped edge-count index.
#3.3 Mutable state
D1 stores lifecycle state that does not participate in the digest:
export interface CasNodeState {
readonly leaseStartedAt: number;
readonly leaseExpiresAt: number;
readonly childRefCount: number;
readonly rootRefCount: number;
}
All fields default to 0.
childRefCountcounts references from immutable metadata of other CAS nodes.rootRefCountcounts references held by document deltas, retained custom snapshots, or other business roots.- Both counts must remain non-negative.
- GC requires both counts to equal zero.
#4. Node readiness
No explicit status column is used. D1/R2 presence defines readiness.
| D1 row | R2 content | Meaning |
|---|---|---|
| absent | absent | unknown |
| present | absent | not ready |
| present | present | ready |
The canonical R2 key is written only after the service has verified content length and the complete logical-node digest. Consequently, presence at the canonical key is the durable proof that upload validation completed. Failed or interrupted uploads must never write unverified bytes to the canonical key; implementations may buffer a bounded node or use a temporary R2 key while validating.
A node must be ready before it can:
- be read;
- receive a root reference;
- be referenced by a newly inserted node;
- be used by a document operation.
A ready D1 row without canonical R2 content is a storage-integrity fault and never falls back to upload instructions. The recoverable inverse is a verified canonical R2 object whose ready D1 transaction was interrupted; a later lease finishes publication.
#5. Digest and identity
Node addresses use the complete 256-bit SHA-256 digest.
sha256(canonical logical node bytes)
The canonical logical bytes include:
- binary format/version and immutable flags;
- own content size;
- content type;
- ordered child digests;
- own content bytes.
Mutable lease and reference-count fields are excluded.
The binary preimage format is specified in CAS Binary Format. The service validates that supplied metadata and uploaded content reproduce the requested digest.
#6. Lease semantics
A lease guarantees that CAS GC will not delete the node before leaseExpiresAt.
It does not guarantee that:
- the node is ready;
- the node will be deleted immediately after expiration;
- an expired upload attempt can still complete.
The state represents the most recent uninterrupted lease period:
leaseStartedAt = start of the current uninterrupted lease period
leaseExpiresAt = current guaranteed protection deadline
When no lease has ever existed, both are 0.
A claim requests a duration, but the service chooses the actual expiry. It may extend continuous leases more aggressively to reduce D1 write frequency.
Successful v2 responses form a strict union discriminated by state:
ready, awaiting_upload, awaiting_replacement_upload, or
validated_awaiting_children. Only ready carries lease timestamps.
Rules:
- A ready node renews or extends its lease without re-uploading content.
- The lease request carries only
leaseDurationMs; canonical bytes go to the returned write-once R2 target. - The client never carries an upload generation or upload ID.
- Leases are not explicitly released.
- Lease state is stored directly on the node row; there is no separate lease table.
#7. Node creation
A node is created or leased through its expected hash. Creation follows these steps:
- A lease call creates or reuses a short-lived internal generation and returns a presigned write-once temporary-object target.
- The caller PUTs one complete canonical node block directly to R2.
- The caller repeats the same lease request. UniCAS checks the 32 MiB limit, computes the complete SHA-256 digest, parses immutable metadata and ordered refs, and persists generation-fenced validation evidence.
- If any child is not ready, the parent remains staged and the response lists every distinct unready child hash. Later parent leases recheck only child readiness.
- Once children are ready, UniCAS writes the immutable canonical object and atomically inserts metadata and edges, increments child counts, establishes the lease, and removes the reservation.
R2 is published before the D1 row. A crash after R2 and before D1 leaves an orphan object; retry is idempotent and completes the row. A D1 row is never committed without canonical R2 content.
No operation may add a reference to a parent that is not ready.
#8. Garbage collection
A node is eligible when:
childRefCount == 0
AND rootRefCount == 0
AND leaseExpiresAt <= now
GC runs through the same Space CAS queue as lease and reference operations.
For each eligible node:
- Exclude nodes with an unexpired upload reservation and re-check eligibility while holding the per-Space mutation gate.
- Delete the R2 object. R2 deletion is idempotent; missing content is allowed.
- In one D1 transaction:
- re-check both reference counts and lease expiry;
- delete ordered edge rows;
- decrement each child's
childRefCountby edge occurrence count; - reject and roll back if any child count would become negative;
- delete the node row.
- Newly zero-referenced children become candidates for a later GC pass.
If R2 deletion succeeds but the D1 transaction fails, the row remains as a not-ready node. A future lease can require re-upload, or a later GC pass can retry deletion and metadata cleanup.
The App-and-Space queue closes the lease/GC race: a lease cannot be granted between GC's eligibility decision and R2 deletion.
#9. Reference-count updates
#9.1 Child references
Child references are immutable node metadata.
- They are incremented transactionally when a node row is first inserted.
- They are decremented transactionally when that node row is collected.
- Duplicate child hashes count as multiple references.
- A child must be ready when the parent row is inserted.
#9.2 Root references
Business systems update root references in a batch:
export type CasReferences = Readonly<Record<CasHash, number>>;
export type CasRefChanges = Readonly<Record<CasHash, number>>;
export interface CasRootRefUpdate {
readonly requestId: string;
readonly changes: CasRefChanges; // signed non-zero integer deltas
}
The canonical service operation is:
POST /v1/cas/root-refs?appId={appId}&spaceId={spaceId}
Authorization: Bearer <capability>
The request body does not carry App, Space, or domain identity. CAS maps the
globally unique verified issuer to App authority, requires query appId to
match that authority, requires one signed grant to select query spaceId and
contain the exact operation permission. No additional namespace is selected.
The update operation:
- validates a non-empty, bounded request ID;
- canonicalizes and hashes the change set;
- checks the idempotency table;
- if the same request ID and payload already succeeded, returns success;
- if the same request ID has a different payload, returns conflict;
- verifies every positively referenced target is ready;
- verifies every resulting
rootRefCountis non-negative; - allocates the next App-global audit revision;
- applies aggregate changes, appends one Space-bearing audit event, updates the App/Space projection, and records idempotency in one D1 transaction.
Every hash in changes must identify an existing D1 node, including hashes with negative deltas. Each delta must be a non-zero safe integer within configured per-request bounds. The service checks addition overflow before applying it. Empty change sets, unknown hashes, non-integer values, overflow, and results below zero reject the entire batch.
The API is a stable CAS service contract. Whether a Gateway exposes it is an independent ingress policy decision.
A deterministic request ID is recommended, for example:
doc:{documentId}:version:{version}:add-refs
doc:{documentId}:truncate:{firstVersion}-{lastVersion}:remove-refs
snapshot:{documentId}:{version}:add-refs
Idempotency is scoped to (appId, spaceId, requestId) and is
required for timeout and retry safety. It does not introduce owner entities;
aggregate root counts remain scoped only by App, Space, and hash. Business
domains own logical-reference lifecycle; CAS stores counts and audit facts.
#9.3 Reconciliation
Reconciliation is an operator-assisted comparison, not an automatic repair:
- The business system exports its intended balances for one stable App and records the source watermark.
- An administrator reads the CAS App balance and ordered events through a consistent audit revision, optionally filtering one Space at a time.
- The operator compares intended and CAS-recorded counts by Space and hash, then uses event request IDs and business records to explain differences.
- A confirmed business bookkeeping error is repaired through a new normal signed-delta operation with its own deterministic request ID.
- The operator reads a new audit revision and verifies the intended result.
Do not update audit projections directly, derive aggregate lifecycle state from audit balances, or silently force the aggregate to match business claims. The offline cutover compares old projections with every authoritative aggregate, preserves old histories in a read-only archive, and records an App checkpoint. It never seeds a sentinel namespace or invents a cross-history order. See the Root Ref migration and replay bridge.
#10. Space client TypeScript API
import type {
AppCasClient,
SpaceNodeLeaseOptions,
} from "@unicas/space-client";
declare const cas: AppCasClient;
declare const spaceId: string;
declare const hash: string;
await cas.readNode(spaceId, hash);
await cas.readMetadata(spaceId, hash);
await cas.readContent(spaceId, hash, { offset: 0, length: 1024 });
const leaseOptions: SpaceNodeLeaseOptions = {
durationMs: 60_000,
signal: null,
};
await cas.leaseNode(spaceId, hash, leaseOptions);
await cas.listRootRefs(spaceId, { limit: 100 });
await cas.usage(spaceId);
await cas.gc(spaceId, { maxNodes: 100 });
An AppCasClient is created with one appId, an asynchronous capability
provider, and an optional immutable-node cache strategy. Every method receives
the target spaceId; one client can therefore serve several Spaces while
keeping the App fixed. No compatibility client translates the removed
Space-bound API, retired Stack/Tenant routes, or credentials.
leaseNode() is the only lease operation. It returns the current lease state
and, when an upload is required, direct PUT instructions. The thin transport
does not create canonical bytes or perform that upload. Use the blob client for
the complete store/retain workflow. readNode, updateRootRefs, and the
remaining operations are listed in the reviewed
TypeScript API reference.
Blob chunk size and index fanout are local client implementation choices, bounded by protocol defaults. They are not sent as trusted lease parameters; CAS validates the canonical body against its configured node byte and ref limits.
#11. Authenticated HTTP API
CAS owns its native Space and App administrator route contracts. @unicas/service
provides one cloud-neutral HTTP actor that matches both protocols and receives
storage/concurrency strategies through explicit platform ports.
@unicas/service-cloudflare wraps that actor as the only production Worker and
public endpoint. The same Worker serves /v1/cas, /admin, MCP/OAuth, and the
admin UI while preserving credential isolation between route classes. D1, R2,
KV, and Durable Object bindings are Cloudflare adapter concerns; keyed actor
ports preserve the single-writer semantics required by Space and App Root Ref
operations. Node, usage, and GC routes retain the canonical /cas
resource-family segment; Root Refs is a sibling Space operation. A Gateway or
other shared ingress may map selected Space operations to another path, but
that mapping and allowlist are not part of the CAS protocol.
Space service routes accept JWT capabilities from configured App issuers.
Each App registers one stable issuer with multiple rotation keys selected by
kid; the issuer maps uniquely to App authority. The Space verifier checks issuer,
key, signature, algorithm, CAS data-plane audience, time bounds, permissions,
and operation-specific claims before any DO, D1, or R2 access. The
issuer-derived App must match the path, and token Space must equal path Space.
The controlled authority registry is cached for 30 seconds; records
older than the 60-second hard stale/revocation bound fail closed when they
cannot be refreshed.
Root Ref writers require the exact update permission in a grant selecting the Space. Unknown legacy claims and caller-supplied forwarding headers cannot introduce an authorization or storage dimension.
Top-level /admin routes use a provider-registry-backed BFF session plus
deny-by-default Platform Access. Google OIDC, Microsoft personal-account OIDC,
and GitHub OAuth adapters resolve an exact External Identity to one stable
Account before credential issuance. Active platform authority or App
membership admits the Account; blockedAt overrides both for every linked
identity. App members retain equal App-local administrator authority, but
membership never grants apps.create or platform.admin. Browser and CLI
requests recheck Account admission on every operation. Remote MCP checks
admission on OAuth grant issuance and every request; delegated OAuth scopes
never substitute for current authority.
Space JWTs are never accepted by admin routes even if they contain
admin-looking scopes, and OIDC admin sessions are never accepted by Space routes. The
@unicas/service-cloudflare worker owns the admin BFF (src/admin-bff): OIDC
callback, secure session, CSRF boundary, and admin BFF routes; the
@unicas/admin-webui package is the browser UI only. Browser code never
receives OIDC client secrets or storage bindings.
An email-bound pending App or platform invitation may start a special provider continuation for an Account without full admission. Google and GitHub can produce short-lived verified-email evidence. Microsoft token email fields are never evidence; the BFF sends a six-digit challenge to the invitation's exact stored address through the Cloudflare Email binding. Challenge state binds the invitation hash, exact External Identity, and authentication event, and stores only a salted code hash. The resulting session may call only the matching acceptance route plus logout. Acceptance consumes challenge evidence and the invitation in one D1 transaction, then rotates the session. General login uses Account admission, not email allowlisting.
Node upload is driven by one hash-addressed lease operation. The lease body contains only the requested duration; canonical bytes go directly to a short-lived, write-once R2 target returned by the service.
#11.1 Read content
GET /v1/cas/nodes/{sha256}?appId={appId}&spaceId={spaceId}
Authorization: Bearer <CAS capability>
Responses:
200with binary content for a ready node;404for unknown or not-ready nodes.
#11.2 Read metadata
GET /v1/cas/nodes/{sha256}/metadata?appId={appId}&spaceId={spaceId}
Authorization: Bearer <CAS capability>
Returns immutable metadata and mutable state. Unknown nodes return 404.
#11.3 Lease or obtain upload instructions
POST /v1/cas/nodes/{sha256}/lease?appId={appId}&spaceId={spaceId}
Authorization: Bearer <CAS capability>
Content-Type: application/json
{"leaseDurationMs":900000}
The client sends the complete canonical node bytes only to the returned R2
target, then repeats this exact lease request. The service validates the full
SHA-256 digest, envelope, content type, ordered child hashes, and size before
publication. The response state is one of ready, awaiting_upload,
awaiting_replacement_upload, or validated_awaiting_children.
{
"state": "ready",
"hash": "...",
"leaseStartedAt": 1787100000000,
"leaseExpiresAt": 1787100900000
}
#11.4 Extend an existing lease
POST /v1/cas/nodes/{sha256}/lease?appId={appId}&spaceId={spaceId}
Authorization: Bearer <CAS capability>
Content-Type: application/json
{"leaseDurationMs":900000}
The same request extends a ready lease without reading or parsing canonical
content. A missing node returns upload instructions rather than 404.
#11.5 Space usage and GC
GET /v1/cas/usage?appId={appId}&spaceId={spaceId}
POST /v1/cas/gc?appId={appId}&spaceId={spaceId}
Authorization: Bearer <CAS capability>
Gateway exposure remains policy-owned. GC is advisory; triggering it does not guarantee that every eligible node is removed in one call.
App administrators can read the same six accounting dimensions aggregated across the App without a Space capability:
GET /admin/apps/{appId}/usage
Cookie: cas_admin_session=<HttpOnly OIDC-backed session>
The read is authorized by App membership and remains available while the App is suspended. Each usage-bearing Space owns one mutable D1 summary row that is updated transactionally with node, lease, storage-observation, and upload- reservation changes. The App query sums only those rows, so its work scales with Space count rather than node count and performs no R2 or internal Space requests. Identical hashes in different Spaces count independently.
Physical bytes and readiness reflect the latest completed service observation
of each canonical R2 object. A bounded scheduled reconciler backfills legacy
rows and revisits the oldest observations. Until every node in the requested
App has a completed observation, the Admin read fails with 503 SERVICE_UNAVAILABLE instead of returning a partial physical total.
#11.6 Root Refs
Business services apply signed non-zero count deltas:
POST /v1/cas/root-refs?appId={appId}&spaceId={spaceId}
Authorization: Bearer <capability>
Content-Type: application/json
{
"requestId": "session:sessionId:version:7:roots",
"changes": {
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": -1,
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": 1
}
}
CAS does not store logical owner identities. cas_nodes.root_ref_count is the
authoritative aggregate. Every newly accepted update atomically changes the
aggregate, allocates an App-global revision, appends one event containing the
affected Space, updates the App/Space/hash projection, and stores the
App/Space/requestId idempotency result. Both aggregate and current projection
counts are non-negative; zero projection rows are omitted.
Logical owner assignment is not part of the CAS contract. The aggregate count and signed-delta API are the only live root lifecycle model.
There is no compatibility route or hidden namespace for Root Ref writes. Old histories remain read-only; unique old requests replay their original revision without applying another delta. New requests use the App-global stream after its explicit checkpoint. The offline cutover and post-write rollback limits are defined in the migration guide.
#11.7 App admin audit API
Root Ref audit reads are formal App-level admin contracts:
GET /admin/apps/{appId}/root-refs
GET /admin/apps/{appId}/root-ref-events
Cookie: cas_admin_session=<HttpOnly OIDC-backed session>
Both reads support an optional exact spaceId filter and include spaceId in
every row/event. Revisions are monotonic per App across Spaces. All App
members can use the MVP admin surface; finer-grained control-plane roles are
deferred.
The admin BFF and UI are served by @unicas/service-cloudflare (src/admin-bff + admin-webui assets). The ordinary Space client
cannot accept OIDC sessions or call admin routes.
#11.8 Canonical binary codec
The portable full-node representation in CAS Binary Format remains available to export/import tooling, offline verification, migration, and fixtures. CAS exposes no portable-node HTTP route or handler.
#12. SValue and SBlob
All document states, queries, operations, and query results use the restricted
SValue model. Binary values are stored nodes represented by branded SBlob
handles:
interface SBlob {
readonly [privateSignature]: true;
readonly hash: string;
}
type SValue =
| string | number | boolean | null | SBlob
| readonly SValue[]
| { readonly [key: string]: SValue };
SValue version 1 is deterministic RFC 8949 CBOR with media type:
application/vnd.unidocs.svalue+cbor;version=1
SBlob is CBOR tag 65536 around exactly 32 hash bytes. Encoding walks values in deterministic order and returns both bytes and ordered direct child refs. Duplicate handles remain duplicate refs. Decoding uses strict limits, a bounded pre-allocation tokenizer, deterministic re-encoding, and byte equality.
For this reserved content type, CAS derives refs from tags and rejects caller-supplied metadata that differs. Doctypes never implement reference extractors.
#13. DocumentType integration
The runtime binds one authenticated SBlob context per document and curries it into the doctype factory:
interface DocumentTypeContext {
makeSBlob(
hash: string,
loadData: () => Promise<SBlobData>,
): Promise<SBlob>;
makeSBlob(data: SBlobData): Promise<SBlob>;
readSBlob(blob: SBlob): Promise<SBlobData>;
}
type DocumentTypeFactory<TDoc, TQuery, TOp> =
(context: DocumentTypeContext) => DocumentType<TDoc, TQuery, TOp>;
The hash-first overload leases an existing node without invoking loadData.
For a missing node it calls the callback once, derives SValue refs when needed,
leases distinct children, verifies the complete logical digest, and uploads.
readSBlob verifies metadata, content length, SValue refs, and digest. Reads and
in-flight promises use a bounded context-scoped cache; returned bytes are copies.
The doctype sees no Principal or Space identity, HTTP, lease, root-count, or CAS metadata API.
Core defines only Context -> DocumentType. A doctype that needs options owns
an outer Options -> Factory function.
interface DocumentType<TDoc, TQuery, TOp> {
init(): Promise<SValueType<TDoc>>;
query(query: SValueType<TQuery>, doc: SValueType<TDoc>): Promise<SValue>;
apply(
operations: readonly SValueType<TOp>[],
doc: SValueType<TDoc>,
): Promise<SValueType<TDoc>>;
formats: Readonly<Record<string, DocumentFormat<TDoc>>>;
defaultFormat: string;
}
Named formats perform external import/export only. TDoc itself is the snapshot
shape. There is no snapshotFormat: the runtime always persists
encodeSValue(TDoc) and always restores it with decodeSValue. Neither
defaultFormat nor any DocumentFormat.save participates in snapshot I/O.
Agent tools are a separate contract:
interface DocumentAgent {
tools: Readonly<Record<string, AgentToolDefinition>>;
instructions: string;
toolCall(name: string, parameters: JsonValue): Promise<AgentToolResult>;
}
The context-bound handler converts JSON DTOs such as {hash} to typed domain
operations containing SBlob. Provider-neutral result content may reference an
SBlob internally; a provider renderer reads it and emits the model vendor's
media representation. SBlob never appears in JSON or directly on the provider
wire.
#14. Delta transaction flow
Each committed version stores a retained SValue delta root. Selected versions also store an independent retained TDoc snapshot root.
Apply is an outbox state machine:
- authenticate the calling service and resolve immutable session identity;
- settle an older pending version and check
baseVersion; - decode and validate already-canonical operations;
- store the operation batch as an SValue delta root;
- run doctype apply against immutable current state;
- optionally store the resulting TDoc as a snapshot root;
- write one local
svalue_pendingrow containing hashes and canonical bytes; - idempotently apply signed deltas that acquire the delta and optional snapshot Root Refs using a deterministic request ID;
- insert local committed rows and delete the pending row;
- publish the new in-memory state and return success.
If CAS times out, pending bytes allow recovery to re-ensure nodes. If local finalization fails after CAS success, recovery retries the same signed-delta request ID and finalizes the pending row on restart. There is no compensating-delete window.
Version 1 does not persist a TDoc head on every delta. Active SBlob leases protect the in-memory state between snapshots. Normal startup loads the latest standalone snapshot and replays retained delta roots. Default snapshot cadence is 20 deltas; the snapshot endpoint may retain the current version opportunistically.
#15. Snapshot and history lifecycle
- Committing a retained delta acquires one Root Ref for its root hash.
- Committing a retained snapshot acquires one independent Root Ref for its root hash.
- Delta and snapshot roots may share descendants; redundant protection is intentional.
- A restore is a retained delta containing
{kind: "restore", doc: SBlob}; replay jumps to that standalone state instead of recording an empty operation. - A clone within the same
(appId, spaceId)partition retains the source snapshot DAG. Cross-Space or cross-App clone requires an authorized recursive DAG copy into the destination partition. - Before future history truncation, the surviving boundary receives a standalone snapshot; removed delta/snapshot roots are aggregated into one idempotent negative Root Refs update.
- Snapshot replacement uses one atomic update containing old
-1and new+1.
DOCX TDoc is a two-level OpenXML Merkle manifest:
interface DocxDoc {
readonly kind: "openxml-package";
readonly files: Readonly<Record<AbsoluteOpcPath, SBlob>>;
}
The doctype uses public Ariadng ZipReader, ZipWriter, and Document.open/save
to convert between the manifest and a cached in-memory Document. Apply clones a
working document and stores changed package leaves; unchanged hashes reuse nodes.
ZIP paths, content types, CRC, entry count, compression ratio, part size, and
total size are bounded and validated.
PSD follows the same separation. Its TDoc is PsdStoredDoc, an SValue tree of
canvas/layer metadata and pixel SBlobs. PsdDoc, which contains resident or
lazy pixel sources, is only a context-scoped editing/render cache. Import and
apply externalize that model back to PsdStoredDoc; export materializes it and
writes PSD bytes. PSD bytes and the doctype-local JSON IR are never snapshots.
#16. Query and transport values
Query results are SValue. Inline Uint8Array and the old base64 QueryValue
escape layer are removed. Binary query state is an existing SBlob or a named
format download.
Query/apply/history accept SValue CBOR. A temporary JSON adapter remains for
values with no SBlob. Responses containing SBlob require an SValue Accept
header and return 406 to JSON-only callers.
Implemented validation includes unit vectors, CAS/SDK tests, Markdown and DOCX restart recovery, rollback, same-partition clone behavior, native SValue image operations, JSON agent tool-hash conversion, provider-rendered multimodal SBlob results, and independent delta plus snapshot retention of shared image blobs.
Deferred operational work:
- automatic GC scheduling and quotas;
- legacy owner-table migration/drop verification and count-repair tooling;
- production migration and reconciliation of legacy snapshot R2/root counts;
- history truncation and document deletion APIs;
- CBOR tag registration before version 1 production persistence.