Skip to content

fix(sessions): fold the refresh begin into the first batch commit - #3080

Open
devin-ai-integration[bot] wants to merge 14 commits into
masterfrom
devin/1791131370-fold-refresh-begin-commit
Open

devin-ai-integration[bot] wants to merge 14 commits into
masterfrom
devin/1791131370-fold-refresh-begin-commit

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • The session temporal refresh pass now folds the "begin" commit into the first projection-batch commit. It plans the begin in a write transaction that is rolled back, projects the first batch against the planned recovery, and then commits the begin and the batch in one write transaction.
  • A streamed message now commits 3 times during refresh instead of 4. The total number of boundary commits decreases from 12 to 11.

Fixes #2939

Motivation

Issue #2939 reports that a streamed message commits 9 times and writes 246 WAL frames for 193 pages. Earlier changes folded the capture span, drain convergence, and receipt into activation. Refresh still committed four times per message: the begin operation row, the first projection batch, the pending relation receipt, and activation. The begin and batch can share a commit without redesigning the write-ahead receipt protocol. This PR implements the state-machine change identified in #3017.

Changes

  • crates/tracedecay-session-temporal-store/src/refresh.rs: plan_session_refresh_begin_result replays the begin in a write transaction that is rolled back, then returns a SessionRefreshBeginPlanV1::Prepared recovery. commit_session_refresh_begin_batch_result replays the same begin in the transaction that commits the changes. It persists the first batch only when the replayed binding still matches the batch. If the binding diverges or the batch is refused, the begin commits alone as BeganOnly. This leaves the same durable running operation that a crash between the two old commits would have left. The shared cursor key is provisioned in a separate transaction before planning because minting the key in a rolled-back transaction would desynchronize the two replays. After a key exists, the provision commit appends no WAL frames. SessionRefreshRecoveryV1 now carries accepted_at, which keeps the operation's created_at ordered before progress.recorded_at.
  • crates/tracedecay-session-temporal-store/src/handle.rs, tracedecay-global-db, and test_registered_impls.rs: add SessionTemporalWriteTxn::rollback so that the plan transaction can be abandoned without committing.
  • crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/worker.rs: PreparedSessionRefresh carries the planned recovery through the pass. apply_prepared_refresh_effect folds the projection effect through commit_session_refresh_begin_batch and preserves Fail, Deferred, and error accounting from the durable paths.
  • Tests: updates the session_store_read_cost boundary expectation, (4,3,4) → (4,3,3). The temporal_refresh suite tail now checks the folded recovery pass.

Test plan

  • bash scripts/require-exact-test.sh cargo test -p tracedecay-session-runtime --features test-helpers --test session_store_read_cost streamed_message_commits_once_per_durability_boundary -- --exact, 1 passed (red before: measured (4,3,4), now (4,3,3))
  • cargo test -p tracedecay --features test-helpers --test session_suite session_runtime::temporal_refresh, 24 passed
  • cargo test -p tracedecay-session-temporal-store --lib, 152 passed
  • cargo test -p tracedecay-session-runtime --lib, 129 passed
  • cargo clippy -p tracedecay-session-temporal-store|tracedecay-global-db|tracedecay-session-runtime|tracedecay --all-targets -- -D warnings, clean
  • cargo fmt --all -- --check, clean

Checklist

  • No secrets, credentials, or .env files included
  • Breaking changes documented (none, internal store API additions only)

Link to Devin session: https://app.devin.ai/sessions/0bf7d9f457e7462784c8852594601226
Open in Devin Desktop: https://app.devin.ai/desktop/session/0bf7d9f457e7462784c8852594601226?variant=devin
Requested by: @ScriptedAlchemy


Devin Review

A streamed message committed four times inside temporal refresh: the
begin's operation row, the first projection batch, the pending receipt,
and activation. The begin now replays inside a rolled-back write
transaction to produce the same recovery a durable begin would have
left, the projector builds the first batch against it, and one commit
writes the begin and the batch together. A replayed begin that no
longer matches the projected batch commits alone and durable recovery
resumes the operation next pass, the state a crash between the two
commits already left behind. The shared cursor key is provisioned in
its own transaction first so both replays read the same key, and the
operation's created_at stays the plan's accepted_at so progress rows
written between plan and commit stay ordered.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@changeset-bot

changeset-bot Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f517819

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

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

devin-ai-integration[bot]

This comment was marked as resolved.

A pending reset deletes the base rows the first batch would project
from, so a batch projected before the begin commits can never match
the replayed begin's post-reset state; the fold always fell back to
BeganOnly and cost an extra pass. Plan now checks for a pending reset
inside the first transaction and commits the begin when one exists,
so durable recovery picks the operation up in the same pass and
projects the post-reset base.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Correct — a pending reset deletes the base rows inside the begin's transaction, so a batch projected before that commit can never match the replayed begin's post-reset state; the fold deterministically fell back to BeganOnly and cost an extra pass.

Fixed in 8c1d347f87: plan_session_refresh_begin_result now checks session_reset_is_pending inside the first transaction (sharing the exact predicate apply_requested_reset uses). With a reset pending it commits the begin durably and returns the new SessionRefreshBeginPlanV1::Begun; the worker disarms the request, counts begun, and the operation joins the same pass's running_session_refreshes, which reads it after begin processing — so the first batch is projected against the post-reset state and persists through the normal durable path. No folded commit is attempted for reset work, and non-reset sessions keep the fold.

This branch has not been deployed

No deployments
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.

perf(sessions): a streamed message commits 9 times, 246 WAL frames for 193 pages

1 participant