
This is a round up of everything that shipped across the JasperFx GitHub organization since the last review on October 12th. It's grouped by theme rather than by tool this time. The Critter Stack continues to receive a lot of community contribution and the people who sent us code and bug reports come first.
I said last time that the pace wasn't sustainable, and yet here we are and things haven't slowed down:
| Oct 12 to Oct 30 | |
|---|---|
| Tagged releases across Wolverine, Marten, Polecat, Fisher, Weasel, and the shared JasperFx libraries | 72 |
| Plus Bobcat 1.0.0 and Alba 9.0.0 RC3 | 2 |
| Pull requests merged across the public repositories | 464 |
| Pull requests from people outside JasperFx | 57, from 18 contributors |
| Issues opened by people outside JasperFx | 88, from 45 different reporters |
A few things from this window already got their own write up, so I'm going to point at those rather than repeat them: the October release wave for CritterWatch 1.1, Bobcat 1.0, AI Skills 1.17, and the first look at Stoat; dead letter failure groups; broadcasting a message to every node; the serverless transactional outbox; rebuilding projections without downtime with warm-up nodes; agent assignment at fleet scale; and the rewritten Wolverine best practices guide.
What's left is still a lot, and it sorts into five themes:
- Not losing messages. A durable inbox and outbox hardening pass across Wolverine 6.51 through 6.58 that closed, by my count, a dozen distinct ways a message could go missing or run twice. Nearly every one of them came in as a community report with a reproduction.
- Performance, from SQL Server statements that were scanning the whole envelope table, to the
runcommand holding a thread pool thread hostage for the life of your container, to a leader heartbeat that was describing every projection in every store every ten seconds. - Shutting down cleanly. One contributor ran Wolverine through an orderly shutdown and filed nine issues about what it logged and what it leaked. We fixed all nine. As I said on DotNetRocks a couple weeks ago, stopping a running system with work in flight has been one of the biggest technical challenges over time and one of my favorite examples of "no, you can't just vibe code this over a weekend."
- More resilient async daemons. Transient failure classification for every store, paused shards that restart on their own, rebuilds that resume from where they died, and projection start floors.
- Multi-tenancy at scale, mostly Marko Lahma's doing, in the per-tenant partitioned events corner of Marten that was purpose built for massive systems and has admittedly been more complicated than we'd hoped upfront.
Alba 9, separately

Alba 9.0.0 RC3 shipped on the 25th, the last release candidate before 9.0 goes stable, and it's the biggest release Alba has had in years: host pooling for parallel integration tests, scenarios addressed by route name, JSON path and problem details assertions, endpoint coverage and authorization audits, time travel, and a rebuilt docs site. All of it is Jaedyn's work, and it deserves more than a section in a roundup, so Alba 9 gets a post of its own this Sunday. The short version until then: if you're on .NET 10, the RC3 packages are on NuGet, and the v9 changelog has the breaking changes to read first.
Thank you
Eighteen people outside JasperFx had pull requests merged in these three weeks. Fifty seven pull requests is the most we've ever had from the community in a window this size, and the quality is what gets me. These are not typo fixes.
And the reports that were good enough to fix from, which in this window is a long list because the durability section below is mostly made of them:
- dbelinsky-accureference filed three of the most consequential durability issues of the month: a read timeout after a durable pop stranding envelopes on a live node, inbox recovery delivering the same envelope twice, and an envelope from a Marten outbox being handled twice after the receiver's handled row expired. Plus a Marten one about a cancelled save leaving its transaction open.
- Laurence Gillian worked through four
[DeduplicatedWithResponse]gaps on Marten, starting with an option to write the claim and the response inside the endpoint's transaction, which is nowClaimInTransaction. And the report that a store with no event types was logging a PostgreSQL error every ten seconds under Wolverine-managed distribution, which led to the leader heartbeat fix below. - Ahmet Ayhan reported messages lost after a RabbitMQ outage, with the intra-batch duplicate and quorum queue delivery limit mechanics worked out, and one slow tenant holding back every tenant's outbox sends.
- billshuck found a failed dead letter write being discarded while the envelope was reported as dead lettered, and an unresolvable tenant pausing a multi-tenant durable listener for every tenant.
- Daniel Weinand found SQL Server envelope ids going out as
sql_variant, which is the 6.59.1 story below, and Azure Service Bus dead letter recovery completing messages a disposed store never wrote. - Derek Flenniken found the HTTP transport answering 200 when the durable receiver failed to store the batch. That one's a real message loss and it's fixed in 6.58.0.
- Marinela Marchitan showed that logical deduplication on an EF Core chain wasn't crash safe, kowalczb that a durable SQS listener never settled its inbox row, and Patrick Gr. Koerkamp that a batch's scheduled retry left its failed members stuck in the inbox.
- Bhanu Prakash Korthiwada measured 60 to 100 MB of native memory per Roslyn compilation in JasperFx's code generation, and AndriiKononenko3 found every disposed Wolverine host staying reachable through its
Meter. Both are fixed and both matter a lot to anybody running big integration test suites. - Jente Vets and Timo Vahtera independently found Marten's high water agent stopping for good when a watchdog restart failed during an outage, and michaeltnguyen reported the daemon dying on a PostgreSQL restart.
- Brad Jolicoeur asked for XML serialization for NServiceBus interop. Shipped in 6.57.0. Daniel Winkler asked to find scheduled messages by a business identifier, and the
SagaIdfilter in 6.59.0 is the first answer. - Kent Cooper on HiLo caching a connection failure after the database came back, Evan Lofback on a custom foreign key name being overwritten under conjoined tenancy, netijoe on
ILongVersionedstill overflowing through anint, wickedmachinator on subclass queries ignoring nested duplicated fields, and Justin Blackwell on--key=valuebeing split at every=, which brokeRunJasperFxCommandsunderWebApplicationFactoryfor any connection string. - tzdevelopteam found both CloudEvents dropping
Envelope.Headersand theruncommand blocking a thread pool thread for the life of the application. Also Pritam Rao, Maksim Vorobev, Ezra Dyck, Fausto Davila, stigrune-hoc, and SERAYU27112, whose reports each turned into a fix named below.
Fifty seven pull requests from eighteen people, eighty eight issues from forty five. Thank you, all of you.
Not losing messages
This is the theme I care about most, and it's the one where the community did the most. Wolverine's durable inbox and outbox are the whole reason to use durable messaging, and over about two weeks we got a run of reports, nearly all with reproductions, describing ways that a message could be lost, handled twice, or stuck. We built a new durable receiver compliance rig on Bobcat, ran it across PostgreSQL, SQL Server, Marten, and EF Core over local, database, RabbitMQ, and SQS endpoints, and it found the rest. Most of this is Wolverine 6.51.3, with the follow-ups in 6.57.0 and 6.58.0. None of it changes an API, and the one schema addition, a nullable envelope_id column on the deduplication table, is optional.
Messages that were being lost:
- The HTTP transport answered 200 while dropping the batch (6.58.0). When a durable local queue couldn't write an incoming batch to its inbox, the batch endpoint still answered 200, so the sender's durable outbox deleted messages that were never stored on either side. It answers 503 now and the sender retries. Derek Flenniken's report. The durable TCP listener had the same shape, answering
Receivedfor a batch its inbox couldn't store, and now answersProcessingFailure. - A CloudEvents endpoint counted a rejected send as delivered (6.56.0), so a durable endpoint aimed at
/_wolverine/invokecould delete an outgoing envelope the receiver never accepted. - A failed dead letter write was discarded silently (6.51.3) while the envelope was reported as moved to the error queue. The durable receiver awaits the write now, and a failed write is never reported as dead lettered. Same family: a disposed message store refuses a dead letter instead of dropping it.
- Outgoing envelopes a transport couldn't map were logged and deleted (6.52.0). On Azure Service Bus, SNS, and Kafka they're dead lettered now instead of being reported as sent.
- A durable batch's scheduled retry was lost (6.51.3). It's held in memory now. Patrick Koerkamp's report.
- Messages lost after a RabbitMQ outage (6.51.3). Ahmet Ayhan worked out two mechanisms: two copies of one envelope in a single batch tripped the tenant write brake and paused the listener, and listener rejects exhausted a quorum queue's delivery limit. Duplicates in a batch are now reported as
DuplicateIncomingEnvelopeExceptionon every relational store,DeferAsyncpublishes before it acks, and there's an opt-inx-delivery-limitfor durable quorum queues with a startup warning if you haven't set one. The 6.51.3 release notes have the detail, and there's a new "Quorum Queues and the Durable Inbox" section headed for the RabbitMQ docs page.
Messages that were being handled twice:
- Inbox recovery reassigned rows without claiming them (6.51.1), so two nodes could recover the same envelope. Recovery claims are atomic and deadlock-free on every store now.
- An envelope sent again from a Marten outbox after the receiver's handled row had expired ran a second time (6.51.3). Every relational queue sender now moves the outbox row atomically for every durable send: PostgreSQL, SQL Server, MySQL, Oracle, and SQLite each got their own pull request. Three of these were dbelinsky-accureference's.
- A requeued failure was dead lettered as a duplicate of itself (6.51.3). Eager idempotency now writes its placeholder inside the handler's transaction on Marten, EF Core, Polecat, and Fisher.
- Logical deduplication on an EF Core chain committed the claim before the transaction (6.51.3), so a crash in between left a claim with no work behind it. The claim rides the EF Core transaction now. Marinela Marchitan's report.
- A
[Deduplicated]claim stranded by a crash could never be retried (6.51.3). The same envelope can take the claim over now, which is the optionalenvelope_idcolumn. - Messages from non-Wolverine producers were handled twice on redelivery (6.57.0). A Kafka record, a NATS JetStream message, a Pulsar message, a Redis Streams entry, or an SQS or Pub/Sub pull subscription message from something that isn't Wolverine has no Wolverine id, so the durable inbox had nothing to deduplicate on. Marko Lahma's fix gives them a stable envelope id derived from the broker's own identity, Kafka's topic, partition, and offset for instance, and 6.58.0 closes the last gap for partitioned inboxes, where the kept
Handledrow sat in a different partition and didn't collide. - A long handler was started a second time because
InboxStaleTimemeasured from when the message was first written rather than when a node claimed it (6.51.0). A node now keeps the clock running for messages it still holds, and scheduled rows are left alone. - A handled message was replayed on every restart (6.51.0) when a durable PostgreSQL queue had a
DbContextenrolled in an ancillary store, or Fisher or Polecat had anAddMessageHandlerhandler committing an ancillary session. The handled mark is written in the store that holds the inbox row now. stigrune-hoc's report.
Messages that were getting stuck:
- A failed or lost durable database queue pop stranded envelopes on a live node (6.51.0), on PostgreSQL by Clay Davis's fix and then SQL Server, MySQL, and Oracle. dbelinsky-accureference's report.
- A durable SQS listener never settled its inbox row (6.51.3) when the broker delivery was already acked, which had been true since 6.40.0. kowalczb's report.
- An unresolvable tenant paused the listener for every tenant (6.55.0). It's treated as tenant-scoped on the single-envelope inbox write now. billshuck's report.
- A Kafka listener with a durable inbox crashed the process with a stack overflow when the inbox database was unavailable (6.56.0), because
DeferAsynccalled back into the receiver inline and the failed write and the defer recursed. A deferred Kafka envelope goes back on its own task after a short pause now, and because Kafka has no native requeue, aRequeuepolicy on a Kafka endpoint redelivers after about a second instead of immediately. SERAYU27112's report, and I'm glad it was a report and not a page.
And the one I'd most like you to know about if you run serverless: Aksel Kvitberg's Durability.UseTransactionScopedDurabilityPassLock (6.58.0) makes the ServerlessWithOutbox durability pass take a transaction-scoped advisory lock instead of a session lock, which is the only kind that's safe behind PgBouncer or Supabase's pooler. It's opt-in and documented on the serverless guide.
Performance
SQL Server envelope statements seek on their indexes again (6.59.1, today). Daniel Weinand found that several of the SQL Server message store's per-envelope statements were scanning the whole envelope table, and those run once per message. Weasel's generic command builder had no mapping for Guid, so every envelope id went out as sql_variant, and SQL Server had to CONVERT_IMPLICIT the primary key on every row. Envelope addresses went out as nvarchar against varchar columns, which under a SQL collation, the Azure SQL default, defeats the index too. And DateTime parameters were typed as Date, so they kept only the date. All three are fixed in the message store, and the new test reads SQL Server's plan cache and fails if any plan contains sql_variant or converts an indexed column. The Weasel mappings underneath are tracked separately, but Wolverine doesn't wait on them. If you're on SQL Server, take 6.59.1. It's a package bump only.
The run command no longer holds a thread pool thread for the life of your application (JasperFx 2.96.2, also today). This one is mildly embarrassing. run waited for shutdown by blocking a thread pool thread. On a container with a CPU limit under one core, ProcessorCount is 1, the pool's minimum is one thread, and run had it. Every burst of work waited about 500 milliseconds for the pool to add a thread. tzdevelopteam saw a 200 millisecond Task.Delay loop wake up nearly a second late on Azure Container Apps. It awaits now, and RunJasperFxCommands() returns a task when the host is already started. This applies to every Critter Stack application, whatever the persistence or transport.
Roslyn metadata references are shared across compilations (JasperFx 2.96.1). Bhanu Prakash Korthiwada measured 60 to 100 MB of native memory per runtime code generation, freed only by finalizers, because every compilation rebuilt a MetadataReference for every reachable assembly. They're cached now. If you've ever watched a test suite's memory climb while Wolverine generated handlers, this is part of why.
Two memory leaks in test suites. Wolverine's runtime never disposed its Meter (6.50.1), and the durable persistence gauges on it hold callbacks back into the runtime, so every disposed host stayed reachable for the life of the process. AndriiKononenko3 traced the retention path. And stopped subscription agents never released their heartbeat timer (JasperFx 2.90.2), which is a substantial leak if you constantly start and tear down IHost instances in testing. We do, which is how we noticed.
The leader's heartbeat no longer describes every projection in every store every ten seconds (6.50.0, plus Marten 9.50.1, Polecat 5.40.1, and Fisher 1.20.2). Assigning event subscription agents only needs each store's shard names and its database topology. It was building the full usage descriptor, which also read the highest event sequence and described every projection, subscription, event type, and tag type, for every store, on every heartbeat. Laurence Gillian's report came with the heartbeat math already done, and the same report fixed Marten logging a PostgreSQL ERROR every ten seconds for a store with no event types. JasperFx 2.87.1 added the narrower TryDescribeDatabasesAsync() and the three stores answer it straight from their tenancy.
LISTEN/NOTIFY for PostgreSQL queues (6.50.0, Clay Davis). Opt in with EnableQueueNotifications() and an idle queue listener is woken as soon as a message is written instead of waiting out its polling interval, 650 milliseconds median down to under 3 in local benchmarks. It's off by default because it holds one extra long-lived connection per database per process. Read the trade-offs before you turn it on.
One slow tenant no longer holds back every tenant's sends (6.52.0). The durable outbox's delete coalescer was shared across tenant databases, so under database-per-tenant one slow database stalled everybody. Stores and deletes are coalesced per tenant database now. Ahmet Ayhan's report.
Timer agents for short-cadence recurring schedules (6.52.0). A recurring schedule that fires more often than every 15 minutes now runs on a timer agent, one per schedule, instead of round-tripping through the scheduled message store every time. Same deduplication id as the scheduled path, pause and resume through IRecurringScheduleControl. This is a behavior change; opt a schedule out with UseScheduledMessages().
Marten stops doing work that can't return anything. Marko Lahma's start-only commit fix (9.50.3): an inline single-stream projection asks storage only for the ids of streams being appended to, so a commit that only starts streams passed an empty id array and still paid a round trip on id = ANY($1), plus a second connection in the default session mode. The same guard removes a wasted round trip from an async daemon batch whose slices all hit the aggregate write cache. His LoadManyAsync fix (9.51.0) is the same idea. Christian Bergum Bergersen's StreamPagedByCursor fix (9.55.0) stops re-encoding every page. And compiled queries now plan from a generated template and refuse values computed from the query object (9.51.0), which closes a class of "the compiled query worked once" bugs.
Agent assignment stopped churning during warm-ups (6.50.0). Anne Erdtsieck-Wijnen's ping-pong fix is covered in the agent assignment post, but it belongs here too: 1,000 to 3,000 ReassignAgent commands a minute down to a partition that stays put unless another node is strictly lighter.
Shutting down cleanly
Tom Longhurst, who you may know as the author of TUnit, ran Wolverine through orderly shutdowns on Azure Service Bus and filed nine issues in two days about what it logged, what it leaked, and what it waited on. Every one is fixed in 6.55.0 through 6.59.0:
- A database batch cancelled by Wolverine's own shutdown was wrapped in
DatabaseBatchCommandExceptionand logged as an error. Genuine failures still are; a shutdown cancellation isn't. - A send that
IHost.StopAsync()cancelled mid-flight was logged as a send failure, twice. A durable agent logs it at Information now, because the envelopes are in the outbox and come back on restart. A non-durable agent logs it at Warning, because those messages really were not sent. Neither counts toward the circuit breaker. - A leadership lock acquisition cancelled by shutdown was logged as an error. Tom's own pull request.
- The durability metrics sweeper outlived host shutdown and threw
ObjectDisposedExceptionagainst a disposedNpgsqlDataSource. Shutdown stops the sweeper and waits for any query in flight now, and removing a durability agent waits for its query too. DurabilityAgent.StopAsync()didn't join its in-flight command, so a command still running at shutdown could outlive the host and fail against a disposed data source. It cancels and waits now.- Azure Service Bus session listeners disposed their
CancellationTokenSourcebefore the receive loop stopped. They join the loop first now. - Stopping a listener waited out an inline retry cooldown. It interrupts it now, and the retry doesn't run after the stop.
- Shutdown didn't wait for handlers still running on a buffered local queue. It does now.
- And two Windows CI findings that were ours to fix, not yours.
Related, from other reporters: Fausto Davila found the SQLite listener cancelling its polling tasks without observing their completion (6.51.2), and database queue listeners now keep a deferral across a stop. Marko Lahma stopped Wolverine disposing a DbDataSource it didn't own, and kept a blocking receive loop on its dedicated thread past its first await.
If your Kubernetes termination grace period has been catching Wolverine mid-sentence, this is the release line to move to.
More resilient async daemons
Anne Erdtsieck-Wijnen's fix for a paused shard that stayed paused until the pods restarted needed one thing the daemon didn't have: a way to tell a failure that will clear on its own from one that won't. That became a small chain of releases across JasperFx and all three stores.
ShardFailure.IsTransient(JasperFx 2.89.0) classifies a shard's failure from its exception graph: aDbExceptionthat says it's transient, a timeout, a socket error, a non-file-systemIOException. Npgsql classifies correctly on its own. Microsoft.Data.SqlClient and Microsoft.Data.Sqlite don't overrideIsTransientat all, so Polecat 5.42.0 added a SQL Server classifier (timeouts, deadlocks, Azure SQL throttling and failover) and Fisher 1.21.0 added a SQLite one (SQLITE_BUSY,SQLITE_LOCKED,SQLITE_PROTOCOL).- The daemon's retry decision consults those classifiers (JasperFx 2.90.1, Polecat 5.42.1, Fisher 1.21.1). Before this, a SQL Server deadlock or a
SQLITE_BUSYthrown from projection code was a poison event on the first throw, and underSkipApplyErrorsit was dead lettered even though it would have applied on retry. Now the shard pauses, the coordinator restarts it, and nothing is dead lettered. - A systemic database failure pauses the shard instead of being dead lettered event by event (JasperFx 2.93.0, Polecat 5.44.0, Fisher 1.22.0, Marten 9.52.0). A missing column or a permissions error would fail every following event the same way, so dead lettering them one at a time was just noise. This is a behavior change under
SkipApplyErrors, and Marten now attributes the failure to the event that hit it instead of reportingOtherwith no event. - A failed rebuild resumes from its committed progress (JasperFx 2.88.0). Every position strategy used to answer Rebuild mode with a rewind to zero, so a restarted rebuild replayed its history over documents it had already built. It starts from the shard's own persisted progress now, proven in all three stores. And a rebuild that throws hands its shard back to continuous execution (Wolverine 6.50.0), where before it left the shard stopped for good. The health check sweep also leaves a shard alone while its projection is being rebuilt, through the new
IProjectionDaemon.IsRebuilding(), so it can't start a second copy of the shard on top of a running rebuild (JasperFx 2.86.0). - The high water agent survives a failed watchdog restart (JasperFx 2.91.0) and restarts on a stalled mark. Jente Vets and Timo Vahtera both found it stopping permanently when the restart itself failed during a database outage. Marten 9.51.0 pins that it survives an outage that hangs connections, and adds
AddMartenHighWaterHealthCheck<TStore>()for stores registered withAddMartenStore, which Jente also asked for. - Projection start floors (JasperFx 2.93.0, Marten 9.52.0, Polecat 5.44.0, Fisher 1.22.0). A rebuild now honors a configured
SubscribeFromSequenceorSubscribeFromTimeinstead of replaying from zero, and the newAsyncOptions.SubscribeFromFirstRun()pins the high water mark as a projection's floor on its first start and persists it. Marten adds amt_event_projection_floorstable on its next migration. Polecat and Fisher make it opt-in withEnableProjectionFloors = true, because it's a new table, and Polecat plans to flip the default in 6.0. Polecat docs. - Preview and advance the high water mark, store agnostically (JasperFx 2.92.0).
PreviewHighWaterAdvanceAsyncshows the stored mark, the committed ceiling, and how many sequence numbers would be skipped.AdvanceHighWaterMarkToLatestAsyncis a compare-and-set against what the operator was shown, and a mark never moves backwards. This is what CritterWatch's "jump-start a daemon held behind a gap" action calls, and it works on SQL Server and SQLite stores now, not just Marten. - Polecat's stale gap detection actually ages gaps (5.43.0). The age of a gap was read from the high water mark row's
last_updated, which every detection cycle re-stamped, so a gap left by a rolled-back append was never skipped while a daemon ran andCatchUpAsynccould hang for its full five minutes. It ages from when the detector first saw the mark held there, as Marten does. - Polecat's extended progression tracking is now actually written (5.41.0). With
EnableExtendedProgressionTrackingon, including through CritterWatch, a running daemon never wrote the heartbeat, agent status, pause reason, running node, or failure columns. It does now. I'd like to tell you we found that one ourselves.
Multi-tenancy at scale
Marten's UseTenantPartitionedEvents mode, one event partition per tenant, had never been pushed as hard as Marko Lahma pushed it this month. Across Marten 9.51.0, 9.52.0, and JasperFx 2.91.0:
- Subscriptions rewind per tenant. A tenant-less rewind under per-tenant partitioning refuses a non-zero floor rather than silently doing the wrong thing.
- A removed and re-added tenant is no longer held at its old per-tenant high water mark, and a re-added tenant's agent started before its first append projects every event.
- Removing a tenant no longer fails with
23503, whether the store has DCB tag rows or a[NaturalKey]aggregate. The tag rows and natural key rows are deleted before the partition drop. - Per-tenant subscription agents seed their progress from their starting position, an empty tenant list comes back before the first tenant is registered instead of
42P01, andMarkEventsAsSkippedis scoped to a tenant. - Sharded tenancy resolves a shard's database id instead of registering the shard as a tenant, and master-table and sharded tenancy let the tenancy's own
AutoCreatepick the apply mode. - Per-tenant timestamp rewinds in Polecat resolve each tenant's own floor (5.43.0). Every one of them used to throw.
Fisher doesn't need any of the per-tenant pieces, because one SQLite file has one event sequence for every tenant in it, and database-per-tenant runs one daemon per file. It did get an opt-in Wolverine-managed event subscription distribution in 6.51.2, so a Fisher-backed fleet can spread its subscriptions the way a Marten one does.
On the Wolverine side, the per-tenant outbox coalescing and the unresolvable-tenant listener fix are above. And Anne Erdtsieck-Wijnen filed a memory pressure issue from managed event subscription distribution at tenants times projections scale, about 52,000 agents with gen2-dominated OOMs during warm-up. That's still open, and the interim advice is in the docs: set MaximumHopperSize to about twice BatchSize. Nobody had run this at 52,000 agents before. We're learning what that looks like in cooperation with a JasperFx client, and the fixes will keep coming.
Marten, the rest of it

Eleven Marten releases, 9.50.0 through 9.55.0. Beyond the themes above:
TombstoneStream, a hard stream delete (9.50.3). session.Events.TombstoneStream(id) queues a single-statement delete of a stream's tag rows, its events, and its stream row, scoped to the session tenant under conjoined tenancy, and it runs at SaveChangesAsync like every other write. The event sequence is never touched and projected documents are deliberately left alone. It's on the shared IEventStoreOperations contract with a compliance suite, so Polecat (5.42.0) and Fisher (1.21.0) have the same thing.
DCB under Native AOT (9.50.3). Tore Hammervoll's RegisterTagType<TTag, TInner>(unwrapper) names the inner type so the generic closes at compile time, and the inferring overloads now carry [RequiresUnreferencedCode]. Because an analyzer warning never reaches a store configured from inside a library, a runtime failure now throws naming the exact line to write. With explicit registrations, tagging, tag rules, EventTagQuery, and FetchForWritingByTags all run from a native binary, and a CI job publishes and runs them. Same API in Polecat and Fisher.
Three Include() defects in one corner (9.50.0). Felix Winterhalter's fix for an Include() joined on the queried document's own id in a query session led to a second 42703 when the query also called Stats(), and then to TotalResults reporting the page size instead of the match count for Stats() plus Include() plus Select() plus Take(). That last one is a value change, to the correct value, so if you compute page counts from it, your counts change.
Application-registered exception transforms (9.52.0, Anne's design). StoreOptions.Advanced.ExceptionTransforms lets you turn a PostgresException with your own SQLSTATE into a typed exception, consulted before Marten's built-ins.
Fixes worth knowing: a cancelled save rolls back and writes its tombstones off the failed save's path (9.51.0), where before it left its transaction open and the inline tombstone write waited on it for the full command timeout. HiLo no longer caches a failed factory build across an outage (9.55.0). Subclass queries use the duplicated column for nested members (9.55.0). A multi-stream projection's revision is no longer truncated through int (9.52.0), so ILongVersioned works as advertised. UndoDeleteWhere and HardDeleteWhere no longer cause a false ConcurrencyException later in the same session (9.52.0). A custom foreign key name survives conjoined tenancy (9.50.3), which is what made overlong foreign key names unfixable. Archiving a tagged stream under archived-stream partitioning no longer fails with a foreign key violation (9.50.3). And an inline array literal in a LINQ Contains() is treated as a value, including a compiled query's own members (9.51.0).
For CritterWatch's explorers (9.52.0, 9.53.0, and the same day in Polecat and Fisher): QueryEventData<T>() is store-agnostic LINQ over the bodies of one event type across every stream, PreviewDocumentJsonAsync is a dry run of a document save that refuses exactly what the save would refuse, UndeleteDocumentAsync restores a soft-deleted document, OrderByMetadata orders a document page newest first, and DocumentTypeRef.IsSoftDeleted tells a console whether a delete is permanent before it does it. All default interface members, all binary compatible.
Polecat and Fisher

Polecat shipped eleven releases, 5.40.0 through 5.46.1, and Fisher eight, 1.20.2 through 1.24.1. Most of both are the shared contract work above landing the same day as Marten, which is the whole point of the shared contract. Beyond that:
Polecat:
- A failed or cancelled save ends its transaction at once (5.43.0), so it no longer holds a stream lock taken by
FetchForExclusiveWritinguntil the session is disposed. Post-commit hooks getCancellationToken.Nonenow, so a token cancelled afterCOMMITno longer fails a committed save. - A save that starts several streams reports the stream that actually collided (5.43.0) in
ExistingStreamIdCollisionException. - The document-side store subject is
polecat://{storename}(5.44.0), as Marten and Fisher report, where it used to return the database URI. If a tool keys onIDocumentStoreDiagnostics.Subject, that's a change. - Two binary compatibility fixes (5.42.0).
TombstoneStreamandCompactStreamAsync<T>are declared onIEventOperationsagain. Both had moved up to JasperFx base interfaces, which compiles fine but throwsMissingMethodExceptionfor already-compiled callers, because .NET doesn't look in base interfaces to resolve those.CompactStreamAsync<T>had been broken for code compiled against 5.10 or earlier since 5.11.0. Nobody noticed, which tells you something about how many people compact streams from a library. - Every content pull request in 5.43.0 was gated locally on the full supervised suite under Bobcat: 3,413 tests on SQL Server 2025 with four workers. That's the dogfooding I was hoping Bobcat would make possible.
Fisher:
ITrackeddocuments carry the session's metadata (1.22.0).CorrelationId,CausationId, andLastModifiedBymap onto their own columns by convention, as Marten'sTrackedPolicydoes. An existingITrackedtype gains three columns on its next migration.- An enlisted session's HiLo block reservation is refused instead of hanging (1.21.2), and a session's post-commit hooks run once, after the commit.
AdvanceHighWaterMarkToLatestAsyncno longer lowers a mark that's abovemax(seq_id)(1.22.0). UseTryCorrectProgressInDatabaseAsyncfor that.- Fisher enrolls 62 of the 63 shared compliance suites now, 747 shared tests, with 2,795 tests green on net9.0 and net10.0. Fisher remains covered by JasperFx support plans.
Weasel

Six releases, 9.45.0 through 9.47.1. Four of the headline items came from outside.
db-patchdrop files leave untouched schemas alone (9.45.0). The drop file used todrop schema ... CASCADEfor every schema the application knew about, so running it as a rollback destroyed schemas the patch never touched. It rolls back only what the patch changed now. This changes what an existing drop file does. Same release: primary keys can be added to and removed from existing tables, reported from Wolverine's SQL Server queue tables switching between a clustered key and a clusteredIDENTITY.- A half-built concurrent partitioned index can be finished (9.46.0, Marko Lahma). Dropping a partitioned index removes only the children attached to it, and the child of the partition the run stopped at was never attached, so the rebuild asked for the same name again and failed with
42P07on every run. Each partition's step now begins with aDROP INDEX CONCURRENTLY IF EXISTSfor that child. - Advisory lock holders are read from the current database only (9.46.0).
pg_locksis server-wide, soFindHolderAsyncreported a lock as held by a neighbor database for a lock this node could have taken. Database-per-tenant deployments and parallel test lanes hit this by construction. - A foreign key is corrected for its linked key once, however many migrations run it (9.46.1, Marko Lahma). Parallel migrations over the same cached
Tableinstances both saw the key one column short and both appended, so42830, and the widened key then lived on the shared instance for every later migration in that process. - Partition child index names stay under 63 characters (9.46.1, Anne Erdtsieck-Wijnen). PostgreSQL truncated them into the suffix, which is the only part that distinguishes one partition's child from another's.
- Indexes are read back in a schema that has to be quoted (9.47.1, Marc Peters). In a schema named
orderoruser, or one with uppercase, no index was read back at all, so every index wasMissingrather thanDifferent, the patch was a barecreate index, and every run after the first failed with42P07. The primary key constraint's name wasn't read back either. One change fixes both. - A per-event Quick append reads back the sequence it drew (9.47.0, vpetrevski), the Weasel half of the Marten tombstone fix above. Opt-in per dialect, defaults to the old behavior.
- No more trim warning from
Weasel.Core(9.46.0, Marko Lahma), found through Quartz.NET's trim canary. And a Firebird catalog read that raced a rewrite is read again instead of reported.
Wolverine, the rest of it

Seventeen Wolverine releases, 6.50.0 through 6.59.1. What hasn't been covered above or in its own post:
Aspire parity: named connections everywhere (6.52.0). Every transport and every relational message store can be registered by connection string name now, so the Aspire-injected ConnectionStrings:postgres works the same way for Wolverine as it does for everything else in your app. NpgsqlDataSource and MySqlDataSource resolve from the container, static tenants can be named, describe has a Named Connection Strings table, and the docs lead with the named form. The Aspire samples moved to Aspire 13.6.3 with a shared ServiceDefaults project. Alba and Marten got Aspire examples the same week.
NServiceBus XML interop (6.57.0). Many NServiceBus systems still use its XmlSerializer, and they no longer have to switch to JSON before a migration to Wolverine can start. Every NServiceBus interop endpoint reads both JSON and XML, choosing per message from the NServiceBus.ContentType header, and NServiceBusXmlSerializer sends XML. Because one endpoint reads both, an NServiceBus estate can move from XML to JSON gradually. Also fixed: NServiceBus replies over RabbitMQ were going to a Wolverine system queue NServiceBus can't reach, so every reply landed in its error queue. Brad Jolicoeur asked for the XML. Docs.
[DeduplicatedWithResponse] claim-in-transaction (6.52.0, 6.54.0). Laurence Gillian's four issues, start to finish: ClaimInTransaction writes the claim and the stored response inside the endpoint's transaction, so a failure after the database commit can't release the claim and let the retry run the work again. There's an application-wide WolverineHttpOptions.DefaultClaimInTransaction, the gaps for [WriteAggregate] binding the aggregate rather than IEventStream<T> are closed, and a concurrent repeat that sees the first request's committed work answers the first response rather than the endpoint's own refusal. Two Marten commit-failure fixes rode along. Docs.
Scheduled messages know about sagas (6.59.0). ScheduledMessageSummary.SagaId and a SagaId filter on ScheduledMessageQuery, so you can query or cancel one saga's pending timeouts. No schema change. Daniel Winkler's ask, and the first step toward finding scheduled messages by a business identifier.
opts.UseHttpTransport() (6.56.0) registers the HTTP transport inside UseWolverine(), so http URIs resolve in ListenForMessagesFrom(), PublishMessage<T>().To(uri), and EndpointFor() without first calling ToHttpEndpoint(). And /_wolverine/invoke answers 415 instead of 500 for a content type with no serializer. A buffered HttpEndpoint aimed at /invoke sends each envelope on its own now (6.55.1) instead of getting a 415 for every batch and retrying forever.
Wolverine.HTTP, beyond John Karam's four: an omitted query, header, or form value binds the parameter's declared default (6.51.2). Single-Value strong-typed id wrappers like record struct OrderId(Guid Value) bind without a TryParse (6.50.0). HTTP handlers mapped directly use ASP.NET Core's JsonOptions without MapWolverineEndpoints(), and a single endpoint can have its own JSON options (6.51.0). Opt-in RejectUnparseableHeaderValues answers 400 with ProblemDetails for a [FromHeader] value that won't parse (6.57.0). And a container-registered service posted on a POST or PUT is resolved from the container on every endpoint (6.50.0, Clay Davis). That one's a behavior change: if you register a type in the container and also post it as a body without [FromBody], add [FromBody].
Error handling: DoThenRetry(action, maxAttempts) works both inline and from a queue (6.52.0, Anne's design). Additional actions chained after ScheduleRetryIndefinitely() or RequeueIndefinitely() apply, including to the open-ended attempts (6.57.0, John Karam). The failure log level is configurable per chain or globally (6.51.0). MessageCausedBy names the handler you wrote, not the generated class (6.55.0).
Operability: AssignmentGrid.Agent.UnplacedReason says why an agent isn't running, AwaitingCapacity, NoCapableNode, Paused, TakeoverHold, or Superseded, and the leader logs agents awaiting capacity (6.52.0). ServiceCapabilities reads each store on its own and reports FailedSections, so a failed section is distinguishable from an empty one. A header-equality filter on DeadLetterEnvelopeQuery is honored by bulk replay and discard across every store. The db-* commands discover the Oracle message store (Pritam Rao's report). The static handler registry keeps handlers your custom discovery rules select, a regression since 6.44.0 (Maksim Vorobev's report). CloudEvents carries lowercase Envelope.Headers as extension attributes. And EndpointName is a typed field on EndpointDescriptor now, so a monitoring tool reading the old properties row should move.
Getting ready for Wolverine 7.0 (6.52.0). I wrote about the .NET 11 wave on my own blog, and the change I most want in Wolverine 7 is one handler chain per handler type instead of combining every handler for a message type into one logical handler. 6.52.0 lays the groundwork: MultipleHandlerBehavior.ClassicCombineIntoOneLogicalHandler is [Obsolete] but still the default, there's a startup warning for combined handlers, an opt-in AssertNoCombinedHandlers() if you want to get ahead of it, a handler.type OpenTelemetry tag on every chain, and logical deduplication scoped per chain. A couple of latent Separated bugs got fixed along the way.
Security. CVE-2026-84008 is a high-severity denial of service in System.Security.Cryptography.Pkcs that arrived transitively through Microsoft.Data.SqlClient, Oracle.ManagedDataAccess.Core, and Polecat. Wolverine 6.50.1, Weasel 9.45.1, and Polecat 5.40.2 reference the patched version directly, and Wolverine 6.50.2 means a NuGet audit comes back clean whichever of them you reference. Marten was never affected. WolverineFx.SignalR also moved off a vulnerable SignalR.Common.
Bobcat and CritterWatch

Bobcat 1.0.0 shipped on the 27th and got its own post. The thing to know from the Wolverine side is that WolverineFx.Bobcat and WolverineFx.Bobcat.EventModel are out of alpha as of 6.55.0 and version with Wolverine. WolverineFx.Bobcat.Http stays alpha until Alba 9 goes stable, because a stable package can't depend on an RC. The Critter Stack vocabulary moved out of Bobcat into WolverineFx.Bobcat at 1.0.0-rc.2, so Bobcat itself takes no JasperFx or Wolverine dependency; if you're coming from 0.x, read the compatibility page first. Every CI suite in Marten, Polecat, Fisher, Weasel, and Wolverine now runs through Bobcat's supervisor, and ./build.sh LocalGate in each repository runs the whole thing on one machine before a push.
CritterWatch 1.1 is in the release wave post. Everything in the "for CritterWatch's explorers" paragraph above exists so that its Document Store Explorer and Event Store Explorer work the same on Marten, Polecat, and Fisher.
Upgrading
The dependency line is Wolverine 6.59.1, Marten 9.55.0, Polecat 5.46.1, Fisher 1.24.1, and Weasel 9.47.1, on JasperFx 2.96.2, with Bobcat 1.0.0 and WolverineFx.Bobcat 6.59.1. Take them together. Alba is 9.0.0-rc.3 if you're on .NET 10 and want the new stuff now, and 8.x otherwise.
There are more behavior changes in this window than usual, so read these first:
- Marten 9.55.0, if you run under a Turkish or Azerbaijani culture and have a document type with an uppercase
Iin its name. Aliases are lowered with the invariant culture now, soInvoicemaps tomt_doc_invoicewhere it used to bemt_doc_ınvoice. Rename the existing tables and upsert functions before you upgrade or your documents will appear to be missing. Every other culture is unaffected. - Marten 9.55.0 changes
StoredDocument.Versionfor projection documents usingUseVersionFromMatchingStreamwith revisions. Anything parsing that token as an integer needs updating. - JasperFx 2.93.0, Marten 9.52.0, Polecat 5.44.0, Fisher 1.22.0: under
SkipApplyErrors, a permanent database error from projection code pauses the shard instead of dead lettering every following event. - Marten 9.52.0 adds the
mt_event_projection_floorstable on the next migration. Polecat and Fisher only add theirs when you opt in. - Marten 9.50.3 shortens foreign key and index names over 63 characters with a hash suffix. If you raised
NAMEDATALENand have a name between 64 characters and your own limit, it'll show up in a migration diff. An empty DCB tag table suffix is refused. - Marten 9.50.0 changes
TotalResultsforStats()plusInclude()plusSelect()plusTake(), from the page size to the match count. - Wolverine 6.54.0 migrates the dead letter table: seven nullable columns, an index, and a
wolverine_dead_letter_historytable, through the normal schema migration. - Wolverine 6.52.0 runs recurring schedules under 15 minutes on timer agents, and dead letters outgoing envelopes a transport can't map. Opt a schedule out with
UseScheduledMessages(). - Wolverine 6.50.0: a container-registered type posted as a request body needs
[FromBody]now. - Weasel 9.45.0:
db-patchdrop files no longer drop schemas, andPrimaryKeyNameis empty for a table without primary key columns unless you set it. - Weasel 9.46.0:
AdvisoryLock.FindHolderAsyncreturnsnullwhere it used to name a holder in another database, and concurrent partitioned index DDL carries aDROP INDEX CONCURRENTLY IF EXISTSper partition. - Polecat 5.44.0: the document-side store subject is
polecat://{storename}instead of the database URI. - Fisher 1.22.0: an
ITrackedtype gains three columns, andAdvanced.AdvanceHighWaterMarkToLatestAsyncno longer lowers a mark. - If you're on SQL Server or Oracle anywhere in the stack, Wolverine 6.50.1, Weasel 9.45.1, and Polecat 5.40.2 are the floor for the Pkcs advisory.
Where this is going
.NET 11 lands on November 10th, and as I wrote on my own blog, I'm leaning toward a wave of major versions right behind it: Marten 10, Wolverine 7, Polecat 6, Fisher 2, Weasel 10, JasperFx 3. Few breaking changes, back ports to the current lines at least through January, and the one structural change I actually want, which is the one-chain-per-handler model that 6.52.0 started preparing for. The durability rig, the supervised CI suites, and the compliance suites across the three stores are what make me think we can do that wave without the three weeks of stabilization the last one took.
In the meantime, the fastest way to get a fix is still to file an issue with a reproduction. Look at the list above if you doubt it. And if you're running any of this at a scale that scares you, that's exactly what JasperFx support plans are for.



