This document tracks the planned direction. Nothing here is committed work — it's a sketch, not a contract. See README.md → "What is this?" for the current scope and CHANGELOG.md for what landed in the most recent release.
-
v0.16.0 is out — the entry points, logging and old defects release, and the largest window so far at 181 commits. Three things landed together.
The root export is core-only (#414).
'actor-ts'used to re-export every subsystem through one barrel, which shipped the testkit in the production entry (#685) and madeimport { ActorSystem }pay for whatever any subsystem pulled in eagerly (#1005). Core stays at the root; the eighteen subsystems each get their own entry (actor-ts/persistence,/cluster,/http, …), the subpaths the documentation already used finally resolve, and the smoke suite loads every declared entry on Bun, Node and Deno (#1003). This is the migration cost of the release —CHANGELOG.mdcarries the note.Logging grew a sink architecture (#1150). The logger wrote to exactly one place; it now fans one record out to as many destinations as you configure — console, rotating files (#1153), and ten log platforms (#1154–#1161) — each with its own minimum level, bounded delivery and a flush on shutdown. Every integration is dependency-free, and nothing about the existing surface changed: a system whose config nobody edited logs exactly what it logged yesterday.
The 50-oldest bug/security wave, 42 of the 50 oldest open defects resolved, including most of the 2026-08-01 security catalogue (#575–#626). The gossip frame gains a required field, so a rolling upgrade across this release does not converge in either direction (#112): an upgraded peer refuses an old node's frames, an old node ignores the new one. Upgrade the cluster in one step, or accept that membership does not converge while both versions are running.
Alongside, the packaging surface was brought in line with what actually ships: no more dangling source maps (#1007, 1262 files and 35 % of the tarball that could not work),
NodeNextresolution so the compiler checks the specifiers the real resolver will (#1008), and@types/nodeno longer a silent requirement for reading the public API (#1006). -
Next window is open (
[Unreleased]). The obvious heads from here: thereference.confexpansion tracked in #887; the residual security items the wave narrowed rather than closed (#112 needs the incarnation identity from #940; #607 needs the eviction policy from #1080); #766, whose titled fix turns out to be insufficient on its own; and the fresh audit round #1166–#1193, which is unstarted and holds severalpriority: highcorrectness defects —PersistentActorhas no fencing (#1166),throttle('pause')livelocks theMicrotaskDispatcher(#1167),KeepMajorityleaves both sides running on an exact tie (#1170), a singleton that exhausts its restart budget is never re-spawned (#1175), andShardCoordinatorignores the configured shard count (#1026). -
~6 250 tests green (unit + multi-node + in-process integration) + 15 real-network multi-node integration scenarios green; open bugs are tracked as
[Bug]issues in the tracker. -
A full audit-catalog of follow-up items is tracked in the issue tracker — security findings, framework features, code-quality refactors. Filter by label
security+severity: <tier>or by title prefix[Security]/[Feature].
-
[Unreleased]— the 50-oldestproduction-goalwave: the 50 oldest openproduction-goalissues — the label that answers "what is still between us and running this for real" — worked as one unit, in nine batches cut by which files they touch rather than by module label. 14 closed, 11 carried forward with the remainder written down, 12 needed a comment and no code, 14 deferred to a second wave with the reason recorded.- Triage before code, and it was the expensive half. Every issue was
read against today's tree by an independent pass asking "are the
acceptance criteria met" rather than "does the defect still exist" —
277 claims in the issue bodies did not survive it, a little over five
per issue. Two issues were already fixed and open only because a commit
said
Refswhere it meantCloses; #461 was already specified in #849; #121's remaining criteria would have reverted the stronger fix that closed it in v0.14.0. Two recorded blockers dissolved on inspection — #290's helper was already on disk, and #638's second half is #928 typecheck:devis green and gated (#540), from 320 errors. The burn-down is not the point: it was hiding five exported declarations that no caller could use.NoopLogger,NoopMetricsRegistryandHashAllocationStrategyeach declared fewer parameters than the interface they implement —implementsaccepts that — sonew NoopLogger().info('hello')did not compile for a user.OtelContextLikehad only optional properties, which trips the weak-type check, so nothing satisfied it, the real OTelContextincluded. Each compiled for the library and broke only for a caller, which is exactly what a configuration that never runs cannot catch- Independent verification found two regressions the wave put into its
own work, both repaired with the failing test written first. #637's
widened trigger set included
MemberUnreachable, which makes the peer that lost contact promote itself while the incumbent — never told it is considered unreachable — keeps its child: a sustained two-host state, the exact condition that issue exists to prevent. It reproduced with the implementer's own test, which asserted the handover and not the invariant its sibling four lines above asserts. Andbun run smokewas left red on Node by a new case that fails four runs in five — a single green run had been taken as evidence - Unreachability is now deliberately not reconciled, and that is the
fix rather than a gap. On the no-lease path "hosted somewhere" and "at
most one" cannot both be had, because reaching the incumbent to ask it to
stand down is precisely what failed. A quorum gate was designed and
rejected: it is a split-brain resolver at a layer with no downing state,
and the two failure detectors fire independently, so a window remains in
which both host. Configure a
leasewhere the invariant must survive a partition - What the wave narrowed rather than closed, so it is not re-derived: #545's POSIX process-group teardown has never executed (the development machine is Windows) and its proof is the first CI run; #539's workflows and #538's nightly cannot be green before a push; #612's rollback floor is in-process and does not survive a restart; #631 fixes the code and leaves existing journals holding the collapsed tags
- Three counts were wrong in the direction that flatters, and got
measured. #663's report claimed four sleeps remained under
examples/and none was a drain sleep; the real figures are 108 delay sites of which 7 were drain sleeps — and the boundary a grep cannot see is thatterminate()drains/userwhile eleven framework actors live under/system, so a sleep crossing that hop is load-bearing. One candidate removal passed on the first run and survived only 2 of 12 repeats, which is how it would have reached CI which files they touch rather than by module label. 42 resolved — 38 closing on this window's push plus four closed by hand — and 12 left open on purpose, each with a comment saying why. The bulk is the 2026-08-01 audit catalogue (#575–#626) plus the four May entries (#112, #118, #121, #132). - Two of the fifty were not defects at all. #118's titled timer leak
cannot happen —
settle()clears and nulls the timer and every latertell()early-returns — and the issue body already retracted it; it was a duplicate of #177, whose two unique clauses were folded across. #132 asked for redaction of a trace id that no framework path ever writes to a log record; #995 owns making that bullet true first. Neither cost a line of code, and finding that out was the point of asking "are the acceptance criteria met" rather than "does the defect still exist" - Five entries are BREAKING, each with a migration note in
CHANGELOG.md. The consequential one is #112:GossipMessagegains a requiredsequence, so a cluster must be upgraded in one step — an upgraded peer refuses an old node's frames and an old node ignores the new field. The others are narrower: object-storage bodies must carry their integrity tag once integrity is configured (#579), the Express and Hono request-body caps drop to match Fastify (#357), a documented-but-inert gRPC deadline starts being enforced (#577), and an ungatedDevTools.mount()now throws (#594) - Independent verification found five regressions the wave itself
introduced, all repaired before the merge, each with the failing test
written first. The worst was #588's own fix: the new inbound-connection
cap armed no handshake deadline on the accept path, so 1024 sockets that
send nothing saturated it and refused every real peer — a defence that
became the attack. Also #597 (one malformed environment variable killed
the whole discovery ladder, including the explicit seed list that does not
read it), #600 (
reset()orphaned an in-flight acquire, so the very end state the issue exists to prevent survived), #610 (the scan was fixed but not the copy — 95 % of the remaining cost) and #586 (a hard throw on@hono/node-wsversionspackage.jsonstill declared supported) - What the wave narrowed rather than closed, stated so it is not
re-derived later: #112's guard holds only while the sender is still a
member — deleting a member drops its high-water mark, so once the sender
is also evicted a recording replays again, and closing that needs #940's
incarnation identity. #607's key bound is necessary but not sufficient
without #1080's eviction policy. #602 deliberately has no HOCON key,
because one would reach
HttpExtension.clientand silently not theHttpClientinsideD1Client— a bound that applies to some clients and not others is worse than none - Eleven release notes were corrected after the fact, most of them overclaims by the wave's own hand. #121's migration note named the wrong safe version (v0.13.0 for v0.14.0), which would have had an operator roll clients out against nodes that crash on every envelope
- Triage before code, and it was the expensive half. Every issue was
read against today's tree by an independent pass asking "are the
acceptance criteria met" rather than "does the defect still exist" —
277 claims in the issue bodies did not survive it, a little over five
per issue. Two issues were already fixed and open only because a commit
said
-
v0.15.0 — mailboxes and channels:
- The unbounded mailbox is the default again (#1148) — BREAKING, and
the reason this window is a minor. A mailbox cannot tell a stale sample
from a control message, so
drop-headon every actor was never confined to the telemetry-shaped workloads it suits.mailboxOverflow/withMailboxOverflowis a realActorOptionsfield now, setting it without a capacity is rejected rather than ignored, andActorCellwarns at 10 000 queued messages and every doubling after - Drop reporting reaches every mailbox (#1149, #661) — the cell
registers its observer after choosing the mailbox, on anything
implementing the new
DropReportingMailboxcontract, so a mailbox supplied throughwithMailboxis no longer invisible toactor_mailbox_dropped_total.MailboxandEnvelopeare exported from the package root, which is what made the documented escape hatch reachable at all EventStreamchannels can bekind-discriminated types (#1143) — a newEventKeymirroringServiceKey/ShardKey, or the bare kind string; class channels are untouched. Publishing something nobody could subscribe to was possible before this- The event stream survives a bad subscription (#1010) — the channel
test, the predicate and
subscriber.tellall run under a guard, so one faulty entry no longer silently stops every subscription registered after it, on every spawn, stop and dead letter - Constants have a placement rule (#1142) — four possible homes,
checked in order, written down in
AGENTS.md; ~300 constants across 130 files audited against it. Every public name is unchanged - A collision predicate on the random-id helpers (#1141, #1146) — an
optional
existscallback that collapses the hand-writtendo/while, bounded at 1 000 draws; three of the framework's own twelve draws now use it, and the other nine are recorded with the reason
- The unbounded mailbox is the default again (#1148) — BREAKING, and
the reason this window is a minor. A mailbox cannot tell a stale sample
from a control message, so
-
v0.14.0 — caps, codecs and lifecycle:
allPersistenceIds+currentPersistenceIdsPaginated(#156) — the read side can now enumerate entities as a live fan-out stream and as a cursor-paginated walk, instead of only as one materialised array. Paging is pushed into the backend wherever a sorted key over ids exists —ORDER BY … LIMITon SQLite and the SQL dialects, anall_persistence_idsclustering range on Cassandra
-
v0.13.0 — names and lifecycle:
Propsremoved (#547) —spawn(MyActor, name); per-actor configuration isActorOptions, a regularXOptionsfamily. The one place in the framework that did not follow that convention- Sharding lifecycle (#892) — idle entities passivate after 5 minutes by
default, empty shards stop with them,
shardPassivationIdleMs; plusShardInfo.resident(#901) Actor.displayName()(#891) — a readable name in log lines and the DevTools tree, settable from the spawn site or at runtime; the path stays the identity everywhere it is an address- Generated names hardened — anonymous actors (#895), reliable-delivery
controllers (#897), the reserved
$prefix (#900), DistributedData quorum ids (#896), object-storage temp paths (#898),ClusterClientidentity (#910) - Cluster correctness — a crossing dial no longer partitions two healthy
nodes forever (#697),
rememberEntitiessurvives a rebalance handoff (#632), handoff buffers are replayed (#893), remembered entities return after an unexpected shard death (#894), remote shard refs route through the owning region (#901) - The TLS listener actually requests a client certificate (#565) — the
documented mTLS recipe had been server-authenticated only, and the
hellohandshake carries no credential of its own
-
v0.12.x window:
- DevTools suite (#445) — embeddable web UI for a running system, seven
panels on one versioned tap protocol behind a
./devtoolsexport. Absorbed the separately-listed live cluster visualizer (#204) - Five more persistence backends (#438) — MongoDB, DynamoDB, MS SQL, libSQL/Turso, Cloudflare D1, each with journal + snapshot + durable-state, on the new relational base layer (#389); CockroachDB + YugabyteDB certified on the Postgres stores (#401)
- Documentation site (#26) — Starlight, with a 1:1 German mirror and a
generated TypeDoc API reference at
actor-ts.dev/api/ - Cluster addressing — a shard is a real actor (
Region → Shard → Entity) withshards()/shardRefFor()/entityRefFor()introspection (#511, #512, #151);SingletonKey/ShardKeydeclare identity on the actor class,cluster.singletonmirrorscluster.shardingandstart()returns a plainActorRef(#523); framework actors moved from/userto grouped/systempaths (#509) - Cluster-correctness follow-ups — cross-node
ask()gets its reply instead of timing out (#517), a role-restricted singleton is hosted by a node that carries the role rather than nowhere (#524), the singleton proxy buffer is bounded (#526), andleader()/KeepOldestdocument the address ordering they actually use (#525) - Core correctness pass from the 2026 audit — the
terminatedsignal is delivered (#448), routers prune dead routees (#449) and reject an unusable pool size (#455), reliable-delivery settles in-flight sends on shutdown (#451), a bounded mailbox keeps its bound while suspended (#407),FailureDetectorthresholds are consistent and validated (#452), and HOCON parsing cannot reach the object prototype (#406)
- DevTools suite (#445) — embeddable web UI for a running system, seven
panels on one versioned tap protocol behind a
-
v0.11.0 window — consistency + validation sweep:
- Repo-wide naming conventions, hard cuts:
Websocketcasing (noWs), abbreviations spelled out (*Cmd/*Msg/*Ack/*Impl/*Ctor, testkitexpectMessage/expectMessageType), one config vocabulary (Options, neverSettings), single-letter locals spelled out OptionsValidator+OptionsError(#274) — fail-fast domain validation on every input path (builder / plain object / HOCON) across brokers, cluster, sharding, discovery, leases, caches, persistence, HTTP middleware/backends, WebSocket routes + policy,CircuitBreaker,BoundedMailbox, and the testkit- Security hardening: WebSocket Origin allowlist (CSWSH defence, WS-2), per-route WebSocket connection cap (WS-5, partial), per-caller identity scope for idempotency keys (HTTP-4), object-storage decompression cap as a store option (#3)
- Repo-wide naming conventions, hard cuts:
-
v0.10.0 — SQL persistence backends + compression levels:
- PostgreSQL backend — journal + snapshot + durable-state (the first SQL-backed durable-state store),
registerPostgresPlugins, optimistic concurrency, indexed tag queries, livepostgres:latestCI suite (#323) - MariaDB backend — same three components via the
mariadbconnector, MariaDB dialect, livemariadb:latestCI suite (#324) - Configurable gzip/zstd compression
levelon the object-storage stores (#322) - zstd compression fixed on non-native runtimes — compress is native-only (Bun / Node ≥22.15),
fzstdis the decompress-only fallback (#321)
- PostgreSQL backend — journal + snapshot + durable-state (the first SQL-backed durable-state store),
-
v0.10.0 — production-readiness audit response, 5 technical points:
- DurableState revision tampering — opt-in HMAC-SHA256 integrity (#116, CRITICAL)
- ClusterClient askId predictability —
crypto.randomUUID()(#120, HIGH) - Master-key rotation sweep race — durable resume tokens + keyring-completeness pre-check (#109, HIGH)
- LeaseMajority split-brain — epoch-gated acquires + release-on-abandon + optional fencing tokens (#142, HIGH)
- Bounded mailbox is now the default — 10 000 /
drop-headwithactor_mailbox_dropped_totalmetric (#310) JsonLogger+otelLoggerfor OTLP-Logs pipelines (#311)- HTTP route middleware +
BearerTokenAuth+IpAllowlist+managementRoutesauth integration (#312) - Real-network multi-node integration tests — docker-compose, tc-netem, 15 scenarios covering cluster primitives end-to-end (#313)
- MultiNodeSpec
enterBarrier— Akka-style cross-node test synchronization (#198, was #47) - Backend
remoteAddresswiring for Fastify / Express / Hono (#312 follow-up)
-
v0.7.0 —
mget/mseton theCacheinterface (#14), MQTT 5.0 user properties + reason codes (#13) -
v0.8.0 — Cluster-management extended HTTP endpoints (#56), Re-encryption sweep + journal-to-journal copy + ClusterClient + WriteConsistency/ReadConsistency
-
v0.8.0 — 8 security-hardening fixes (wire-frame DoS cap, FS path-traversal guard, Memcached CRLF, gossip version cap, snapshot seq integrity, WebSocket frame cap, hello-handshake hijack defense, idempotency body-fingerprint)
Inbox— synchronous adapter for non-actor callers — #181DeathWatch.watchWith— custom termination message — #159ShardCommandtypes —StartEntity,GetShardStats,GetClusterShardingStats— #151
- Persistent reminders (Orleans-style durable timers) — #168
- Stateless workers — per-node pool of identical activations — #170
- Saga / process-manager with compensations — #179
- Placement strategies (PreferLocal / HashBased / ActivationCountBased) — #169
- Deterministic-simulation-testing (FoundationDB-style seeded virtual-time replay) — #200
- LLM agents as supervised, durable actors — epic #421 (absorbed the earlier LLM-tool-call-as-actor sketch; tool dispatch is #423)
- Per-entity chaos injection — #206
- Streams DSL subset (
SourceQueue,MergeHub,BroadcastHub) — #147 - Full
Source/Flow/Sinkcomposition DSL — #54 (the long-horizon track; #147 is a deliberate ~5 % subset, not a replacement for it) - WASM / edge-runtime subpackage — #209
- Distributed transactions (Orleans-style ACID 2PC across grains) — #171
- Multi-DC clustering with DC-local failure detection — #149
- Performance benchmarks vs JVM actor frameworks (#27) —
benchmarks/has the micro-benches; what is missing is the side-by-side comparison run
- "Auto-magic" cache invalidation by tag / pattern — known bug-source; invalidation stays explicit via
cache.delete(...). - Backwards-compatibility guarantees of any kind — pre-1.0.
- Pull requests — not accepted; well-shaped issues are (the issue tracker is the contribution channel, see
README.md→ "Roadmap & status").
→ Full catalog: GitHub issues, filterable by title prefix [Security] / [Feature] and the security / severity: … labels.