Skip to content

feat(sdk,core): offload large trigger payloads via object storage - #4470

Closed
deepshekhardas wants to merge 20 commits into
triggerdotdev:mainfrom
deepshekhardas:feat/large-payloads-object-storage
Closed

feat(sdk,core): offload large trigger payloads via object storage#4470
deepshekhardas wants to merge 20 commits into
triggerdotdev:mainfrom
deepshekhardas:feat/large-payloads-object-storage

Conversation

@deepshekhardas

Copy link
Copy Markdown

Offloads large trigger payloads via object storage. See original PR #3785.

Deploy Bot and others added 20 commits February 2, 2026 16:16
- Include reproduction scripts for Sentry (triggerdotdev#2900) and engine strictness (triggerdotdev#2913)
- Include PR body drafts for consolidated tracking
- Include reproduction scripts for Sentry (triggerdotdev#2900) and engine strictness (triggerdotdev#2913)
- Include PR body drafts for consolidated tracking
When the underlying logical-replication client errored (e.g. after a
Postgres failover), the runs and sessions replication services logged
the error and left the stream stopped. The host process kept running,
the WAL backed up, and ClickHouse silently fell behind.

Both services now run a configurable recovery strategy on stream errors,
defaulting to in-process reconnect with exponential backoff so a fresh
self-hosted setup heals on its own:

- "reconnect" (default) re-subscribes via the existing subscribe(lastLsn)
  path with exponential backoff (1s -> 60s cap, unlimited attempts), which
  re-validates the publication, re-acquires the leader lock, and resumes
  from the last acknowledged LSN.
- "exit" calls process.exit after a short flush window so a host's
  supervisor (Docker restart=always, systemd, k8s, etc.) can replace the
  process.
- "log" preserves the historical behaviour.

Per-service strategy + exit knobs are env-driven via
RUN_REPLICATION_ERROR_STRATEGY / SESSION_REPLICATION_ERROR_STRATEGY plus
matching *_EXIT_DELAY_MS / *_EXIT_CODE. Reconnect tuning is shared
across both services via REPLICATION_RECONNECT_INITIAL_DELAY_MS /
_MAX_DELAY_MS / _MAX_ATTEMPTS (0 = unlimited).
Addresses PR review feedback:

- LogicalReplicationClient.subscribe() can throw before its internal
  "error" listener is wired up (notably when pg client.connect() fails
  mid-failover). The reconnect strategy's catch block only logged, so
  recovery silently stopped. Now also calls scheduleReconnect(err) — the
  pendingReconnect guard makes it idempotent if an error event was also
  emitted.
- Reject negative values for the new replication-recovery env vars and
  cap exit codes at 255.
- Convert the new ReplicationErrorRecovery{Deps,} interfaces to type
  aliases to match the repo's TypeScript style.
- Tighten the reconnect dep comment to drop a stale "lastAcknowledgedLsn"
  reference (the wrapper-tracked resume LSN is what callers actually pass).
- Restore process.exit after service.shutdown() in the exit-strategy
  test so a delayed exit timer can't terminate the test worker.
LogicalReplicationClient.subscribe() can resolve without throwing or
emitting an "error" event when leader-lock acquisition fails — it just
calls this.stop() and returns. The reconnect callback now checks
isStopped after subscribe() and throws so the recovery handler can
schedule the next attempt instead of silently giving up.
…rough handle()

The previous post-subscribe() isStopped check was always true on the
happy path: subscribe() calls stop() up front (setting _isStopped=true)
and only resets the flag inside the replicationStart event, which fires
asynchronously after subscribe() returns. So the check threw on every
successful reconnect, the catch rescheduled, the next attempt tore down
the just-built client, and the cycle continued — replication briefly
worked between teardowns, which is why the integration test passed.

Replace it with the correct nudge: subscribe to leaderElection and call
the recovery handler on isLeader=false. That's the only subscribe()
exit path that doesn't either throw or emit an "error" event (the other
silent-return paths emit "error" first via createPublication/createSlot
failures).
The previous commit routed leaderElection(false) through handle(), which
under the exit strategy schedules process.exit. In a multi-instance
deployment that turns lost leader election — a normal operational state
— into a restart loop: exit, supervisor restarts, election fails again,
exit, and so on.

Add a dedicated notifyLeaderElectionLost() on ReplicationErrorRecovery
that the reconnect strategy treats as another retry trigger, while
exit and log strategies no-op. Wire the wrapper services through the
new method.
fix(webapp): auto-recover replication services after stream errors
…riggerdotdev#3785)

- Add ioSerialization utilities for payload serialization
- Add api-type.test.ts for new API types
- Update trigger payload schema with IOPacket support
- Update shared.ts to import IOPacket type
- Add changeset for @trigger.dev/core and @trigger.dev/sdk

Closes triggerdotdev#3785
@changeset-bot

changeset-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d95d2bc

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@deepshekhardas, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d6444eb7-b62c-4877-8785-6dc52d460d2e

📥 Commits

Reviewing files that changed from the base of the PR and between 14824b0 and d95d2bc.

📒 Files selected for processing (34)
  • .changeset/fix-console-interceptor-2900.md
  • .changeset/fix-docker-hub-rate-limit-2911.md
  • .changeset/fix-github-install-node-version-2913.md
  • .changeset/fix-orphaned-workers-2909.md
  • .changeset/fix-sentry-oom-2920.md
  • .changeset/large-trigger-payload-offload.md
  • .server-changes/replication-error-recovery.md
  • apps/webapp/app/env.server.ts
  • apps/webapp/app/services/replicationErrorRecovery.server.ts
  • apps/webapp/app/services/runsReplicationInstance.server.ts
  • apps/webapp/app/services/runsReplicationService.server.ts
  • apps/webapp/app/services/sessionsReplicationInstance.server.ts
  • apps/webapp/app/services/sessionsReplicationService.server.ts
  • apps/webapp/test/runsReplicationService.errorRecovery.test.ts
  • consolidated_pr_body.md
  • packages/cli-v3/src/cli/common.ts
  • packages/cli-v3/src/commands/deploy.ts
  • packages/cli-v3/src/commands/dev.ts
  • packages/cli-v3/src/commands/login.ts
  • packages/cli-v3/src/commands/update.test.ts
  • packages/cli-v3/src/commands/update.ts
  • packages/cli-v3/src/deploy/buildImage.ts
  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/cli-v3/src/entryPoints/dev-run-worker.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/cli-v3/src/entryPoints/managed-run-worker.ts
  • packages/cli-v3/src/utilities/sourceMaps.test.ts
  • packages/cli-v3/src/utilities/sourceMaps.ts
  • packages/core/src/v3/consoleInterceptor.ts
  • packages/core/src/v3/schemas/api-type.test.ts
  • packages/core/src/v3/schemas/api.ts
  • packages/core/src/v3/utils/ioSerialization.ts
  • packages/core/test/ioSerialization.test.ts
  • packages/trigger-sdk/src/v3/shared.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Hi @deepshekhardas, thanks for your interest in contributing!

This project requires that pull request authors are vouched, and you are not in the list of vouched users.

This PR will be closed automatically. See https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md for more details.

@github-actions github-actions Bot closed this Aug 2, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 11 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines 214 to +217
} finally {
await watcher?.stop();
process.off("SIGINT", signalHandler);
process.off("SIGTERM", signalHandler);
await cleanup();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Local development mode shuts itself down immediately after starting

The local dev session is torn down (await cleanup() at packages/cli-v3/src/commands/dev.ts:217) as soon as startup finishes, because the call that is supposed to keep the command alive returns instantly, so tasks stop running right after trigger dev starts.
Impact: Developers running the dev command lose their local task worker immediately and cannot execute tasks.

Why the wait never blocks and what the new cleanup tears down

startDev returns waitUntilExit = async () => { } (packages/cli-v3/src/commands/dev.ts:312), a no-op. So await devInstance.waitUntilExit() resolves immediately and control falls into the new finally block, which now calls cleanup()devInstance.stop(). That stop() (packages/cli-v3/src/commands/dev.ts:316-320) calls the dev-session stop() which removes the build destination, stops bundling, and calls runtime.shutdown() (packages/cli-v3/src/dev/devSession.ts:247-255), plus stops the config watcher and deletes the lockfile.

Before this PR the finally block only stopped the config watcher (await watcher?.stop()), leaving the dev session and its worker runtime alive, so the command kept working. Cleanup should only run on actual exit/signal, or waitUntilExit must resolve only when the session ends.

Prompt for agents
In packages/cli-v3/src/commands/dev.ts, devCommand now awaits devInstance.waitUntilExit() and then unconditionally runs cleanup() in a finally block. However startDev returns waitUntilExit as an empty async function (a no-op), so the await resolves immediately and cleanup() tears down the dev session (devSession stop -> runtime.shutdown, bundling stop, watcher stop, lockfile removal) right after boot. Previously the finally block only stopped the config watcher, so the session survived. Either make waitUntilExit return a promise that only resolves when the dev session actually ends (e.g. resolved by the signal handler or session exit), or don't run the teardown from the finally path until the session has genuinely finished.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +202 to +210
const signalHandler = async (signal: string) => {
logger.debug(`Received ${signal}, cleaning up...`);
await cleanup();
process.exit(0);
};

try {
const devInstance = await startDev({ ...options, cwd: process.cwd(), login: authorization });
watcher = devInstance.watcher;
process.on("SIGINT", signalHandler);
process.on("SIGTERM", signalHandler);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Ctrl+C cleanup handler never runs because an earlier handler exits the process first

The new interrupt handler that is supposed to clean up child processes (process.on("SIGINT", signalHandler) at packages/cli-v3/src/commands/dev.ts:209) is registered after a global handler that exits the process synchronously, so the cleanup never executes and worker processes can still be left behind.
Impact: Pressing Ctrl+C can still leave orphaned worker processes running, which is the exact problem the change intends to fix.

Handler ordering

installExitHandler() runs at CLI startup (packages/cli-v3/src/cli/index.ts:46) and registers process.on("SIGINT", () => process.exit(0)) (packages/cli-v3/src/cli/common.ts:88-95). Node invokes signal listeners in registration order; the first listener calls process.exit(0) synchronously, terminating the process before the later async signalHandler in devCommand is invoked (and before any awaited cleanup could complete anyway).

A fix requires either removing/avoiding the global exit handler for the dev command, or performing cleanup from within that handler.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

export const UpdateCommandOptions = CommonCommandOptions.pick({
logLevel: true,
skipTelemetry: true,
ignoreEngines: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Option to ignore Node engine checks during install has no effect

The new setting that is supposed to relax Node version checks during dependency installation (ignoreEngines: true at packages/cli-v3/src/commands/update.ts:21) is never read when packages are installed, so deployments still fail when the project declares a stricter Node version.
Impact: Deployments on build servers with a mismatched Node version keep failing, and the newly added tests asserting the flags are passed will fail.

Missing plumbing into installDependencies

UpdateCommandOptions now picks ignoreEngines (packages/cli-v3/src/commands/update.ts:18-21) and deployCommand passes { ...options, ignoreEngines: true } (packages/cli-v3/src/commands/deploy.ts:262), but updateTriggerPackages still calls await installDependencies({ cwd: projectPath, silent: true }) (packages/cli-v3/src/commands/update.ts:261) with no args. The new test file packages/cli-v3/src/commands/update.test.ts:74-112 asserts args: ["--no-engine-strict"] (npm), ["--config.engine-strict=false"] (pnpm), ["--ignore-engines"] (yarn) and [] otherwise — none of which the implementation produces.

Prompt for agents
packages/cli-v3/src/commands/update.ts accepts a new ignoreEngines option (picked into UpdateCommandOptions, set to true by deployCommand) but never uses it. The call to installDependencies({ cwd: projectPath, silent: true }) needs to pass package-manager-specific args derived from the detected package manager when options.ignoreEngines is true: npm -> --no-engine-strict, pnpm -> --config.engine-strict=false, yarn -> --ignore-engines, otherwise an empty array. See the expectations in packages/cli-v3/src/commands/update.test.ts.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +3071 to +3083
async function prepareTriggerPayload(
payload: unknown,
apiClient: ApiClient,
taskId: string
): Promise<IOPacket> {
const payloadPacket = await stringifyIO(payload);
return await conditionallyExportPacket(
payloadPacket,
createTriggerPayloadPathPrefix(taskId),
undefined,
apiClient
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Large trigger payloads now fail outright when the upload step is unavailable or unauthorized

Oversized payloads are uploaded to storage before triggering (conditionallyExportPacket(...) at packages/trigger-sdk/src/v3/shared.ts:3077-3082) with no fallback to sending them inline, so a trigger that used to succeed now throws whenever the upload step is rejected.
Impact: Triggering a task with a large payload fails against older servers, or when using a public/JWT access token, even though the trigger request itself would have been accepted.

Two concrete rejection paths

exportPacket calls client.createUploadPayloadUrl(filename) which hits PUT /api/v2/packets/... (packages/core/src/v3/apiClient/index.ts:579-590) and throws if storagePath is missing (packages/core/src/v3/utils/ioSerialization.ts:172-176).

  1. Older self-hosted servers have no /api/v2/packets route, so the presign 404s and the trigger throws — previously the server offloaded the payload itself in DefaultPayloadProcessor (apps/webapp/app/runEngine/concerns/payloads.server.ts:20-45).
  2. The v2 packets route calls authenticateApiRequest(request) with no options (apps/webapp/app/routes/api.v2.packets.$.ts:21), which rejects public keys and public JWTs (apps/webapp/app/services/apiAuth.server.ts:119-125), while the trigger route itself sets allowJWT: true. So SDK triggers authenticated with a public token now 401 on payloads ≥128KB.

Consider catching upload failures and falling back to sending the packet inline (the server still offloads it), or restricting offload to clients using secret keys.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 310 to +311
this._isShuttingDown = true;
this._errorRecovery.dispose();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Stopped replication service can resurrect itself through a pending reconnect timer

A scheduled retry of the database replication stream is only cancelled during full shutdown (this._errorRecovery.dispose() at apps/webapp/app/services/runsReplicationService.server.ts:311), so stopping the service without shutting it down lets a queued retry re-open the stream afterwards.
Impact: A replication service that was explicitly stopped can silently reconnect and keep consuming the replication slot.

Paths that don't dispose

stop() (apps/webapp/app/services/runsReplicationService.server.ts:336-344) and teardown() (:346-354) call this._replicationClient.stop()/teardown() and clear the ack interval, but never call this._errorRecovery.dispose(), and they don't set _isShuttingDown, so the recovery helper's isShuttingDown() guard (apps/webapp/app/services/replicationErrorRecovery.server.ts:92) is false when the pending timer fires and reconnect() calls subscribe() again. The same applies to SessionsReplicationService.stop()/teardown() (apps/webapp/app/services/sessionsReplicationService.server.ts:320-344).

Prompt for agents
RunsReplicationService.stop()/teardown() and SessionsReplicationService.stop()/teardown() stop the underlying LogicalReplicationClient but do not dispose the new error-recovery helper, and they do not set the shutting-down flag. A reconnect timer scheduled before the stop will therefore fire and call subscribe() again, resurrecting a service that was intentionally stopped. Call _errorRecovery.dispose() (or otherwise mark the service as not-running so isShuttingDown() returns true) from both stop() and teardown() in both services.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +10 to +18
// Mock dependencies
vi.mock("nypm");
vi.mock("pkg-types");
vi.mock("node:fs/promises");
vi.mock("@clack/prompts");
vi.mock("std-env", () => ({
hasTTY: true,
isCI: false,
}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New tests rely on mocks instead of the required real-dependency test approach

The added test suites replace real modules with fakes (vi.mock(...) at packages/cli-v3/src/commands/update.test.ts:11-51), which the repository's testing rules explicitly forbid.
Impact: These tests violate the mandated testing conventions and validate mock wiring rather than real behaviour.

Rule and locations

CLAUDE.md: "We use vitest exclusively. Never mock anything - use testcontainers instead." AGENTS.md: "Tests should avoid mocks or stubs and use the helpers from @internal/testcontainers when Redis or Postgres are needed."

Violations: packages/cli-v3/src/commands/update.test.ts:11-51 mocks nypm, pkg-types, node:fs/promises, @clack/prompts, std-env, and several internal modules; packages/cli-v3/src/utilities/sourceMaps.test.ts:6-10 mocks source-map-support and stubs process.setSourceMapsEnabled.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread consolidated_pr_body.md
Comment on lines +1 to +11
# Consolidated Bug Fixes

This PR combines fixes for several independent issues identified in the codebase, covering CLI stability, deployment/build reliability, and runtime correctness.

## Fixes

| Issue / Feature | Description |
|-----------------|-------------|
| **Orphaned Workers** | Fixes `trigger dev` leaving orphaned `trigger-dev-run-worker` processes by ensuring graceful shutdown on `SIGINT`/`SIGTERM` and robust process cleanup. |
| **Sentry Interception** | Fixes `ConsoleInterceptor` swallowing logs when Sentry (or other monkey-patchers) are present by delegating to the original preserved console methods. |
| **Engine Strictness** | Fixes deployment failures on GitHub Integration when `engines.node` is strict (e.g. "22") by passing `--no-engine-strict` (and equivalents) during the `trigger deploy` build phase. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Change bundles many unrelated fixes and ships a stray planning document

A summary document listing nine independent fixes is committed to the repository root (consolidated_pr_body.md:1-11), confirming that this change bundles multiple unrelated issues, which the contribution rules disallow.
Impact: The change is hard to review and revert, and an internal planning file is published in the repository.

Rule

CONTRIBUTING.md: "Important: We only accept PRs that address a single issue. Please do not submit PRs containing multiple unrelated fixes or features. If you have multiple contributions, open a separate PR for each one."

The diff mixes SDK payload offloading, webapp replication error recovery, CLI dev/deploy/update changes, Docker Hub login, source-map handling and console interception, alongside consolidated_pr_body.md, which should not be committed at all.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +3085 to +3088
function createTriggerPayloadPathPrefix(taskId: string): string {
const safeTaskId = encodeURIComponent(taskId);
return `trigger/${safeTaskId}/${Date.now()}-${Math.random().toString(36).slice(2)}/payload`;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Payload is uploaded before idempotency/dedupe is evaluated, leaving orphaned objects

prepareTriggerPayload runs before the trigger request, so a large payload is always uploaded even when the trigger is deduplicated by an idempotency key, debounced, or when the request subsequently fails. The generated key (trigger/<taskId>/<timestamp>-<random>/payload.<ext>) is not tied to a run id, so nothing later can associate or clean up these objects (server-side offloads use ${friendlyId}/payload.json, see apps/webapp/app/runEngine/concerns/payloads.server.ts:32). Expect slow growth of unreferenced objects in the packets bucket; a retention policy or run-scoped key may be needed.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 288 to +298
this.logger.info("Leader election", { isLeader });
if (!isLeader) {
// Failed leader election doesn't throw or emit an "error" event —
// subscribe() just emits leaderElection(false), calls stop(), and
// returns. Route through a dedicated handler so only the reconnect
// strategy acts; the exit strategy must not restart-loop when
// another instance holds the lock.
this._errorRecovery.notifyLeaderElectionLost(
new Error("Failed to acquire replication leader lock")
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Losing the leader lock triggers an endless reconnect loop by design — verify the log volume

leaderElection(false) now routes into scheduleReconnect, which retries forever by default (REPLICATION_RECONNECT_MAX_ATTEMPTS default 0 = unlimited) and logs each attempt at error level (apps/webapp/app/services/replicationErrorRecovery.server.ts:84-88). In a normal multi-instance deployment every non-leader instance will emit an error log every up-to-60s indefinitely, which is a meaningful change from the previous silent behaviour. Consider logging leader-lock contention at a lower level than error.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +262 to +269
this._errorRecovery = createReplicationErrorRecovery({
strategy: options.errorRecovery ?? { type: "reconnect" },
logger: this.logger,
reconnect: async () => {
await this._replicationClient.subscribe(this._latestCommitEndLsn ?? undefined);
},
isShuttingDown: () => this._isShuttingDown || this._isShutDownComplete,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Reconnect does not restart the acknowledge interval or reset transaction state

The reconnect callback only calls this._replicationClient.subscribe(this._latestCommitEndLsn ?? undefined), unlike start() which also creates the acknowledge interval and starts the flush scheduler. That is fine when recovery follows a mid-life stream error (both are already running), but if the very first start() fails at leader election, the recovery path resubscribes without those having been (re)initialised in the failure case, and any partially accumulated _currentTransaction from the dropped stream is not cleared before the new stream replays from the last commit LSN — worth confirming the client emits a fresh begin before any further events.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants