Skip to main content
S2 integrates with Mastra through @s2-dev/mastra-pubsub. The package provides an S2PubSub implementation for Mastra durable agents. Each durable topic maps to an S2 stream. When S2PubSub appends an event, the S2 sequence number becomes the event’s Mastra index. Live delivery and replay therefore use the same index, which lets Mastra replay and deduplicate events without a separate counter. A durable agent reconnecting after a browser refresh

Install

The package requires Node.js 22.13 or later and @mastra/core 1.46 or later in the 1.x release line. @mastra/core is a peer dependency. Create a basin with Create Stream on Append and Create Stream on Read enabled. These settings let S2PubSub create a run stream when it first writes or reads the topic. You can create the basin in the dashboard or with the CLI:
Set the basin, access token, and model provider key:

Configure Mastra

Create an S2PubSub and pass it to the Mastra instance that registers your durable agent:
src/mastra/index.ts
src/mastra/agents/research-agent.ts
Registered durable agents use the Mastra instance’s pubsub. By default, S2PubSub uses S2 for topics beginning with agent.. This covers per-run topics such as agent.stream.<runId> and the per-thread topics Mastra uses for coordination. Topics outside that prefix, and events published with localOnly, use the inner local transport.

Reconnect to a run

Starting a durable-agent stream returns a runId. Keep that ID where a reconnecting client can retrieve it.
After a browser refresh or dropped connection, call observe with the same runId:
Pass an offset when the caller already has part of the stream. The offset is the first event index to replay, so use 42 if index 41 was the last event processed:

How persistence works

S2PubSub extends Mastra’s CachingPubSub, but S2 is the authoritative state for durable topics:
  1. publish serializes the event and appends it once to the topic’s S2 stream. Read sessions deliver the stored record to subscribers in the publishing process and in other processes, restoring its seqNum as the event index.
  2. subscribe opens a read session at the live tail. If that session reconnects, it resumes from the next exact sequence number.
  3. subscribeFromOffset opens a read session at the requested offset. The same session replays retained records and then follows new records, avoiding a replay-to-live handoff gap.
  4. getHistory reads retained events from an offset and restores each record’s seqNum as its event index.
  5. clearTopic stops this instance’s subscriptions, clears local state, and requests deletion of the corresponding S2 stream. Deletion failures are logged rather than thrown.
The adapter does not keep event history, a replay cache, or a durable cursor in process memory. Call await pubsub.close() during graceful shutdown to cancel active read sessions.

Delivery semantics

Publishing rejects if S2 does not acknowledge the append; durable topics do not fall back to a process-local event. The S2 SDK’s default append retry policy is at-least-once, so an ambiguous timeout can produce a duplicate record. If that is not acceptable, create the S2 client with appendRetryPolicy: 'noSideEffects' and pass the client to S2PubSub:
S2-delivered callbacks run one at a time per subscription. S2PubSub awaits each callback before reading the next record, which preserves stream order and applies backpressure when a callback is slow. A callback that never settles stalls that subscription. If a callback rejects, the adapter logs the error and considers the record delivered; broadcast observers do not provide acknowledgement or callback-level redelivery. Events use JavaScript’s JSON.stringify semantics. Before appending, the adapter validates the serialized record, so unsupported values such as data: undefined, functions, symbols, bigints, or circular data reject publish instead of leaving an unreadable record in the stream. Optional nested properties whose value is undefined are omitted.

Distributed leases

getLeaseProvider() returns the S2-backed lease provider used by Mastra’s signals runtime to elect one owner per thread key across processes. The lease owner, expiry, and nonce are encoded in the thread stream’s S2 fencing token, so lease operations remain atomic without an in-process cursor or a separate lease stream. S2 fence commands are filtered out of event history and live delivery, but they still consume sequence numbers. Resume from last.index + 1, not from the number of events received. Do not trim a thread stream while its lease is held. clearTopic also deletes the stream, so do not call it for an active thread topic.

Cleanup and retention

When Mastra cleans up a run, it calls clearTopic, which deletes that run’s S2 stream. Mastra’s default cleanup window is 30 seconds; set cleanupTimeoutMs to 0 if the stream should remain until explicit cleanup or S2 retention removes it. If a process exits before cleanup, its stream can remain. You can apply an age-based retention policy and delete_on_empty in the basin’s default stream configuration to remove these orphaned streams. Keep the retention period longer than the maximum run time and reconnection window you support. A request whose offset has already been trimmed fails instead of silently returning partial history.

Options

new S2PubSub(config, options?)

S2 Lite

Install the S2 SDK for its environment helper:
To use S2 Lite, set S2_ACCOUNT_ENDPOINT and S2_BASIN_ENDPOINT, then pass the parsed endpoints to S2PubSub:

Example

The package repository includes a runnable example adapted from Mastra’s durable-agents example: examples/durable-agents.