Release 2026-09-29
This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at 2026-09-29.md, and llms.txt lists every page.Part of the changelog.
Patch release: Orleans.Lattice, Orleans.Lattice.Api.Backup, Orleans.Lattice.Api.Data, Orleans.Lattice.Api.Mcp, Orleans.Lattice.Api.Mcp.Telemetry, Orleans.Lattice.Api.Mcp.Telemetry.Azure, Orleans.Lattice.Api.State, Orleans.Lattice.Api.Telemetry, Orleans.Lattice.Api.TreeAdmin, Orleans.Lattice.Backup, Orleans.Lattice.Caching.AzureBlob, Orleans.Lattice.Dashboards, Orleans.Lattice.GrainIndex, Orleans.Lattice.Replication, Orleans.Lattice.Replication.Grpc and Orleans.Lattice.Scaling advance to 9.8.1 from the release/9.8 line. Every other package stays at 9.8.0. Packages that depend on a patched package still require its 9.8.0, so reference a patched package directly to pick up its fixes.
Upgrading from 9.8.0. A rolling upgrade is supported. While versions are mixed, WAL GC on a 9.8.1 silo that asks a 9.8.0 leaf to bank its pin falls back to a starvation drive, as 9.8.0 does; finish the upgrade before starting a resize. The leaf snapshot load-failure counter's residual reason is now unclassified (was faulted), so reason-filtered alerts should match both. Replication gRPC now refuses a call carrying no stamped origin header, which every 9.8 sender stamps. The resize and snapshot fix for a tree an adaptive split has grown (#3880) is not in this patch: it depends on tree-lifecycle work queued for the next minor.
Added
WAL - GC reclaimable distance and terminal breach. Each GC shard scan publishes its floor-to-head distance in offsets, and a tree over its byte ceiling with an available floor that reclaims nothing for 10 consecutive passes advances a primed terminal-breach counter. (#3149) (
Orleans.Lattice9.8.1,Orleans.Lattice.Dashboards9.8.1)WAL - Drain-lag holders named while lag persists. A tree above the materialiser lag threshold repeats its structured warning naming the lowest-cursor consumers, at most every
LatticeOptions.WalDrainLagHolderLogInterval(default 10 minutes; null keeps edge-only logging). (#2505) (Orleans.Lattice9.8.1)
Changed
Performance - OR-map provenance decoding. A delta encodes a key's surrogate once per dot group, a churned key's live-dot test uses a sorted counter index, the state decoder emits each key's events contiguously instead of sorting every event, and the emitters presize their sink. (#3675, #3701, #3833, #3850) (
Orleans.Lattice9.8.1)Performance - Register and flag provenance decoding. A multi-value register decode no longer builds a live-replica hash set per delta, nor copies and sorts its projection before returning it, and a flag read returns a UTF-8 literal. (#3675, #3833, #3850) (
Orleans.Lattice9.8.1)Performance - CRDT dot scans. The observed-remove dot primitives and the OR-map, OR-set and RW-set provenance decoders walk their dot lists as spans rather than through the list indexer, and every live-dot read stops at the first survivor instead of counting all of them. (#3748, #3772, #3793) (
Orleans.Lattice9.8.1)Performance - UTF-8 transcodes. An aggregation row encodes a short string once, gated on a compile-time bound rather than a per-string virtual call, and the WAL pin-routing and blob cache-key hashes transcode in one pass, dropping a heap buffer on oversized consumer ids. (#3793, #3833) (
Orleans.Lattice9.8.1,Orleans.Lattice.Caching.AzureBlob9.8.1)Performance - Leaf range scans. A range read sorts only when a pending write appended out of order and drops bound re-tests the scan window already enforces, and a bounded scan retires its lower bound once met instead of retesting every row. (#3760, #3772) (
Orleans.Lattice9.8.1)Performance - Projection digest and index fingerprint. The projection digest and the grain-index fingerprint transcode each string once rather than twice, and a single-replica vector clock is fed straight through instead of rented and sorted per row. (#3760, #3772) (
Orleans.Lattice9.8.1,Orleans.Lattice.GrainIndex9.8.1)Performance - Predicate filter evaluation. Predicate filtering validates an admitted row's JSON only after it evaluates true, rather than scanning every candidate twice, and the leaf scans, view projections and atomic-write guard settle fast-path eligibility once, not per row. (#3692, #3701) (
Orleans.Lattice9.8.1)Performance - Leaf snapshot frame reads. The frame codec re-validated its 24-byte header on every read helper, inside per-row and per-probe loops. It is now read once and threaded through: a lower-bound seek is 76% faster, hydration admission 22%, and the aggregate walk 64%. (#3881) (
Orleans.Lattice9.8.1)Performance - Leaf key comparison and row accounting. The UTF-8 comparator behind every seek now decides from the first differing byte, decoding only when that byte is non-ASCII; ASCII keys compare 16x faster. A hydrated row reuses the key length the frame states instead of re-scanning it. (#3891) (
Orleans.Lattice9.8.1)Performance - Durable pin fan-out. Tree purge and consumer unregister now clear the WAL materialiser pin keys in one concurrent round instead of walking them, replacing 2 * shards + 1 sequential grain round trips - seventeen at the default shard count - with one. (#3891) (
Orleans.Lattice9.8.1)Performance - CRDT delta fold enumeration. Eight delta-fold and shape-registry sites walk a span over the concrete backing array or list instead of the read-only list interface, which boxed an enumerator per call. The coalescing fold is 18% faster and allocates up to 896 fewer bytes. (#3850) (
Orleans.Lattice9.8.1)Performance - GetMany strict-pass retry capture. The single-shard
GetManyAsyncstrict-pass retry closure no longer allocates a nested parent capture, removing 24 bytes from every call. (#3678) (Orleans.Lattice9.8.1)Performance - Entry-history member decoding. An entry-history page now reuses one single-element provenance delta buffer across the revisions it decodes, instead of allocating a fresh array per revision. (#3692) (
Orleans.Lattice9.8.1,Orleans.Lattice.Api.State9.8.1)Performance - CRDT read-path projections. The grow-only and remove-wins whole-set reads and the sequence provenance projection no longer materialise through a hidden-count iterator or a discarded tuple array; each now fills one exactly-sized destination from a single scan. (#3647) (
Orleans.Lattice9.8.1)Performance - Data-plane CRDT collection reads. The data API's OR-Set, RW-Set and OR-Map whole-collection reads now resolve survivors in one scan into an exactly-sized destination, instead of walking the add-map twice and materialising through hidden-count iterators. (#3654) (
Orleans.Lattice9.8.1,Orleans.Lattice.Api.Data9.8.1)Performance - CRDT dot-coverage liveness reads. A churned observed-remove slot whose cancelling dots all carry one replica now collapses that list to its highest counter instead of rescanning it per dot, across the shared liveness primitives and the OR-Set and RW-Set live-member projections. (#3726) (
Orleans.Lattice9.8.1)Performance - CRDT merge-time compaction.
Compactno longer pairs each add list with a hashed tombstone-map probe per element, nor compacts a tombstone list twice; the OR-Set and RW-Set maps are now swept independently, one pass each. (#3748) (Orleans.Lattice9.8.1)Docs - Unresolved-prepare ledger risk. The ledger's metric description, runtime warning and docs now say Azure Table's row cap bounds its growth while SQLite's larger limit can exhaust activation reads, and recommend the beyond-cap alert on every profile. (#2830) (
Orleans.Lattice9.8.1,Orleans.Lattice.Dashboards9.8.1)Core - Single reminder teardown path. An unreachable PurgeComplete arm is removed from the tree-deletion reminder handler, leaving the early guard as the one teardown path, now pinned by reminder-dispatch tests. (#2347) (
Orleans.Lattice9.8.1)Core - Dead members removed.
BPlusLeafGrain.MinUnresolvedPrepareOffset, the cursor unpin's unreachablerethrowarm and the uncalledShardRootGrain.TryForwardShadowWriteAsyncare removed, their stale docs corrected, and the live paths pinned by tests. (#2405, #2469, #2488) (Orleans.Lattice9.8.1)Observability - Snapshot load failures no longer claim a cause. The leaf snapshot load-failure counter's residual reason is
unclassifiedinstead offaulted, since a wrapped activation failure can hide an OOM; reason-filtered alerts should match both values during rollout. (#2404) (Orleans.Lattice9.8.1,Orleans.Lattice.Dashboards9.8.1)Container - Cgroup readers. The silo and the ONNX embedder compile one dependency-free set of cgroup CPU and memory readers, a gate confines cgroup reads to it, and three unreachable
cpu.maxparser clauses are removed with every parse outcome pinned. (#2817, #2819, #2828) (Orleans.Lattice9.8.1)Docs - Grain await convention. The grain instructions now state when an await in a grain carries an explicit ConfigureAwait and when it does not, and three semaphore waits are brought in line with it. (#3891) (
repository-wide,Orleans.Lattice9.8.1)
Fixed
Scan - Settled page reuse could serve a stale transactional outcome. A reused scan page was guarded against writes and TTL expiry but not against a transaction decision changing by the clock alone. Reuse is now refused when the leaf read resolved pending transactional writes. (#2823) (
Orleans.Lattice9.8.1)Leaf - A range delete hydrated the whole leaf. A foreground range delete enumerated every cached row and detached the leaf's snapshot frame. It now enumerates only the requested range, keeps the frame, and hydrates only the blocks that overlap it. (#2841) (
Orleans.Lattice9.8.1)Observability - set_many stage timers mixed two write paths. The shard-root
set_manystage timers carry anoperationtag separating conditional from unconditional batches, and the conditional path gains its ownorleans.lattice.set_many_where_predicate.durationenvelope. (#2687) (Orleans.Lattice9.8.1,Orleans.Lattice.Dashboards9.8.1)WAL - A write-idle leaf froze the GC floor at a stale pin. A checkpoint persist published its pin before its own snapshot capture, and only a replay-permit drive republished it. The pin now publishes after the capture, and the coverage-lag check and GC sweep bank it without a permit. (#3599) (
Orleans.Lattice9.8.1)WAL - GC floor repair stalled behind pinned holders. A write-idle leaf never persisted a residual checkpoint advance, a pass that trimmed while still retaining discarded the holder sample and its block ages, and concurrent touches could spend the one free replay slot above the floor. (#3608, #3609, #3610) (
Orleans.Lattice9.8.1)Replay - Permit share and disjoint range deletes. The GC replay share is sized from permits still in circulation, not the configured ceiling, and a DeleteRange disjoint from the replaying leaf no longer takes a ledger slot or clamps the checkpoint. Exempt-arrival admission is now pinned. (#3610, #3601, #3299) (
Orleans.Lattice9.8.1)Replication - Deferred and dead-lettered entries could be lost. A custom applier's batch default dropped a receive-fence deferral, a causal-buffer eviction kept its dedupe reservation so its replay was discarded as a duplicate, and a replay the fence deferred removed its parked entry. (#3629, #3630, #3757) (
Orleans.Lattice.Replication9.8.1)Scan - An uncapped point-in-time cursor pin expired after 60 seconds. Setting
MaxCursorSnapshotPinTtltoTimeout.InfiniteTimeSpanfloored the pin toTxDecisionRetention, or expired it at once with retention off; it now disables the cap as documented. (#3631) (Orleans.Lattice9.8.1)Core - Bulk-load begin admitted a populated tree. Its emptiness probe read diagnostics cached for up to 5 s and counted a shard it failed to sample as empty. It now drops the cached reports, samples the shards afresh, and refuses to begin while any shard is unsampled. (#3650, #3680) (
Orleans.Lattice9.8.1,Orleans.Lattice.Api.TreeAdmin9.8.1)Replication - A resumed bootstrap dropped rows above its cursor. A crash-resume or transient retry re-opened the snapshot export bounded at the highest HLC applied so far, dropping unapplied rows stamped above it. Every attempt now exports the whole snapshot; the overlap applies as LWW no-ops. (#3656) (
Orleans.Lattice.Replication9.8.1)Shard - Tree delete, recover and purge skipped split-added shards. They walked only the pinned ShardCount, so keys an adaptive split moved stayed readable and writable after DeleteTreeAsync and kept their state after a purge. They now walk every shard index the tree has allocated. (#3657) (
Orleans.Lattice9.8.1)Core - A completed online snapshot kept mirroring its source. An Online SnapshotAsync never released its source shadow-forward, so later source writes reached the destination, a second online snapshot or resize was refused, and deleting the destination failed source writes. (#3658) (
Orleans.Lattice9.8.1)Core - A resized tree lost its registration, configuration and snapshots. Purging a first resize's retired copy unregistered the live tree, the alias swap reset its PublishEvents, history and retention-ceiling overrides, and a snapshot copied the retired shards. Each now acts on the live copy. (#3741, #3742, #3743) (
Orleans.Lattice9.8.1)Atomic - Reads fail closed when the transaction registry is unreachable. A read meeting a prepare whose outcome the registry cannot supply throws the retryable
LatticeTransactionOutcomeUnavailableException, and a multi-key or streaming read no longer accepts an unverifiable probe as stable. (#2215, #3641) (Orleans.Lattice9.8.1)Leaf - A faulted cache refresh hid keys the leaf holds. A registry fault mid-refresh left a cleared or unmerged leaf-cache mirror still marked fresh, so same-silo reads answered absent for held keys until the leaf wrote again. A faulted refresh no longer marks it fresh. (#2412) (
Orleans.Lattice9.8.1)WAL - A short bank lift was graded as a healed floor. GC arm 2 credited any bank lift as releasing the floor, even a few entries to a stale checkpoint far behind the head. A lift is now graded against the partition head, and one that falls short escalates to a checkpoint drive in the same pass. (#3649) (
Orleans.Lattice9.8.1)Dashboards - 22 panels queried series the exporter never emits. Their tokens used the container's unit spelling where
AddPrometheusExporternames the series differently, so the panels rendered blank. The tokens now use its names, and the naming gate resolves only the family it emits. (#3260) (Orleans.Lattice.Dashboards9.8.1)Core - Queue ids could be reissued after a crash. A queue that drained, or lost its newest entry, before its coalesced head-cursor flush cold-started below ids it had already issued. The next id is now made durable before the newest entry's row is deleted. (#3681) (
Orleans.Lattice9.8.1)Config - Reference autoscaler rules could never scale out. The shipped KEDA and ACA rules set
targetValue1, which asks for at most the current replica count, and the reference architecture queried a series nothing exports. The rules now use 0.5 and the exported series. (#3679) (Orleans.Lattice.Scaling9.8.1)Atomic - Multi-key reads surfaced an indeterminate prepare.
GetManyAsync, key and entry scans, counts and stats now resolve every pending override through the same visibility gate as a point read, so a prepare a point read hides is hidden on every multi-key path too. (#3665) (Orleans.Lattice9.8.1)Shard - Healing and hot-shard sampling stalled on a cold silo. The healing orchestrator and hot-shard monitor latched themselves running before registering their keepalive, so a still-initializing reminder service left them with no timer. Each now arms its timer first and retries the keepalive. (#3682, #3713) (
Orleans.Lattice9.8.1)WAL - A GC pass that resolved no provider read as idle.
orleans.lattice.wal.gc.passesgains a zero-primedno_partitionsarm for a pass whose silo resolved no pinned WAL provider, so a misplaced tree no longer reports an arm that asserts health. (#2465) (Orleans.Lattice9.8.1,Orleans.Lattice.Dashboards9.8.1)Replication - The restore write fence and VC seeder missed live shards. Both addressed
{tree}/0..ShardCount-1, skipping an adaptive split's target and, behind an alias, naming the retired copy. They now follow the tree's live routing, and the fence lifts exactly the shards it engaged. (#3750, #3751) (Orleans.Lattice.Replication9.8.1)Query - Tree structure omitted split-added shards.
GetTreeStructureAsynclisted shard roots0..ShardCount-1of the pinned count, so an adaptive split's target and every key routed to it were missing. It now lists every shard the routing map reaches. (#3752) (Orleans.Lattice.Api.State9.8.1)Atomic - A cross-tree write repeating a key never resolved. A tree slice naming one key twice, including a
Setand aDelete, was admitted, then refused by that tree's sub-saga on every keepalive, parking the other trees' writes. It now throwsArgumentExceptionbefore anything is staged. (#3756) (Orleans.Lattice9.8.1)Backup - A full backup's cut recorded HLC 0. The consistency cut read an anchor the core always stamps as zero; it now records the highest HLC the capture read, so the first increment on a full base pins the WAL at that frontier while it drains. (#3758) (
Orleans.Lattice.Backup9.8.1)Backup - Capturing a tree with a folded shard failed. The topology step digested shards
0..count-1, so once shard healing had folded a shard away it named a missing index and every backup threw. It now follows the routing map, which also gives the true virtual slot count. (#3887) (Orleans.Lattice.Backup9.8.1,Orleans.Lattice9.8.1)Replication - Bootstrap dropped the TTL of committed rows. The default snapshot export left every committed row's expiry at 0, so whole-tree bootstrap and the anti-entropy fallback installed TTL keys on the peer as durable. Committed rows now carry the source entry's absolute expiry. (#3802) (
Orleans.Lattice.Replication9.8.1)Core - Batch writes skipped the write bounds.
SetManyWherePredicateAsync, single-tree atomic batches and the cross-treeSetManyAtomicAsyncextension now enforceMaxKeyLength,MaxValueSizeBytes,MaxLiveKeysandMaxEstimatedByteslikeSetManyAsync, atomic ones before a saga starts. (#3803, #3852, #3889) (Orleans.Lattice9.8.1)Replication - Inbound peer contact went unrecorded for single-entry pushes. The dead-letter decorator's single-entry and per-entry retry paths bypassed the applier's only recording site, so inbound gauges and the inbound-silence health signal missed low-rate peers. Both paths now record contact. (#3848) (
Orleans.Lattice.Replication9.8.1)Observability - Replication wire-version gauges had no tenant tag.
wire_version.negotiatedandwire_version.downgrade_activenow carry the derivedtenanttag like every other replication instrument, so tenant-scoped telemetry sees a tree's mixed-version peers. (#3853) (Orleans.Lattice.Replication9.8.1)Core - History continuation pages misreported the trim point. On the WAL-window history fallback, a truncated continuation page named its own first revision, or zero when it returned none, as
EarliestAvailable. Every truncated page now names the oldest still-readable revision. (#3877) (Orleans.Lattice9.8.1)Replication - Leaf re-replay missed a trimmed partition. Once an earlier partition spent the shared read budget, later ones went unexamined, so a trimmed one read as untrimmed and re-replay ran past a WAL gap instead of reporting
wal_trimmed. Their trim point is now probed. (#3877) (Orleans.Lattice.Replication9.8.1)Config - NaN ratios passed range checks. A NaN
LatticeViewOptions.ThrottledBatchRatiowas accepted, draining one entry per throttled pass, and a NaNRetainedBytesAdvisoryRatiozeroed every storage-pressure threshold. The validator now rejects NaN and the collector falls back to0.8. (#3877) (Orleans.Lattice9.8.1,Orleans.Lattice.Scaling9.8.1)
Security
Security - Read-only callers were advertised every mutating data tool. The data group never overrode its per-tool minimum, so the session filter fell back to the coarse group mask and became a no-op; a bare read grant listed all nineteen destructive tools. (#3863) (
Orleans.Lattice.Api.Mcp9.8.1)Security - Replication peers could act under another cluster's origin.
PushandGetPeerHighWaterMarktrusted the body-declared origin, so any mesh-secret holder could advance or read a third cluster's high-water mark. Both now bind it to the stamped caller, as the manifest exchange did. (#3800) (Orleans.Lattice.Replication.Grpc9.8.1)Security - A saga peer authorized itself from the request body. With the origin header absent, the saga service read the coordinator cluster id from the body, so any mesh-secret holder could name an authorized peer and drive
Prepare/Commit/Abort. Unstamped calls are now refused. (#3893) (Orleans.Lattice.Replication.Grpc9.8.1)Security - Omitting a header bypassed the replication origin gate.
Push,ExchangeContentManifest, andGetPeerHighWaterMarkallowed a call carrying no stamped origin, so a peer could poison or read a third cluster's cursor by not stamping itself. Absent is now refused. (#3893) (Orleans.Lattice.Replication.Grpc9.8.1)Security - Replication credentials were not bound to a cluster. The accepted-secret set carries no peer attribution, so origin checks compared caller-chosen values. New opt-in
BindCredentialToOriginClusterrequires the presented secret to be the one configured for the claimed origin. (#3893) (Orleans.Lattice.Replication9.8.1,Orleans.Lattice.Replication.Grpc9.8.1)Security - A view row decoder trusted a peer-supplied count. Under
ShipViewa view tree is replication-enrolled, so its aggregation rows reach the decoder from a peer. It pre-sized a map from a wireInt32and read past a truncated row; counts and reads are now bounded. (#3784) (Orleans.Lattice9.8.1)Security - Restore authorization was skippable, late, and leaky. The facade skipped its gate when the target was unresolvable, the engine dispatched the cross-cluster saga before authorizing, and the admission probe was ungated and disclosed stored size and shard count. (#3747) (
Orleans.Lattice.Api.Backup9.8.1,Orleans.Lattice.Backup9.8.1,Orleans.Lattice.Replication9.8.1)Security - A comment hid a metric from the allow-list. The PromQL grouping-list scanner counted parentheses blind to comments and quoted strings, so an unmatched
(in either swallowed the rest of the query and hid the aggregand's selector from the deny-all metric gate. (#3686) (Orleans.Lattice.Api.Telemetry9.8.1,Orleans.Lattice.Api.Mcp.Telemetry9.8.1)Security - Telemetry tools were ungated. The MCP telemetry tools ran caller-supplied PromQL against the metrics backend without consulting the telemetry capability, so any caller able to reach the group could read every series. Each tool now authorizes before any backend work. (#3645) (
Orleans.Lattice.Api.Mcp.Telemetry9.8.1)Security - An asserted tenant was never validated. The MCP region catalog scoped its answer to the caller-supplied active-tenant assertion without validating it, so any caller could enumerate another tenant's routable regions. The assertion is now validated and a refusal fails closed. (#3645) (
Orleans.Lattice.Api.Mcp9.8.1)Auth - A tree-scoped rule granted a cluster-wide capability. MCP discovery read the telemetry bit off an Allow rule at any scope, so a grant on a single tree conferred the scopeless telemetry capability. Cluster-wide-only operations are now carried only from a cluster-wide rule. (#3645) (
Orleans.Lattice.Api.Mcp9.8.1)Security - MCP telemetry on Azure resolved the unpatched facade.
Orleans.Lattice.Api.Mcp.Telemetry.Azureis republished with no code change, so it requires the patchedOrleans.Lattice.Api.Telemetry9.8.1 instead of resolving 9.8.0. (#3686) (Orleans.Lattice.Api.Mcp.Telemetry.Azure9.8.1)