Skip to content

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

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

devin-ai-integration[bot] wants to merge 2 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

  • Fold the session temporal refresh "begin" commit into the first projection-batch commit: the refresh pass now plans the begin inside a write transaction that is rolled back, projects the first batch against the planned recovery, and commits the begin and the batch in one write transaction.
  • A streamed message now commits 3 times inside refresh instead of 4 (11 total boundary commits, down from 12).

Fixes #2939

Motivation

Issue #2939 reports a streamed message commits 9 times / 246 WAL frames for 193 pages. After earlier folds (capture span, drain convergence, receipt-into-activation), refresh still committed four times per message: begin operation row, first projection batch, pending relation receipt, and activation. The begin→batch fold is the remaining commit that collapses without redesigning the write-ahead receipt protocol; this PR lands the state-machine change #3017 identified.

Changes

  • crates/tracedecay-session-temporal-store/src/refresh.rs: plan_session_refresh_begin_result replays the begin in a rolled-back write transaction and returns a SessionRefreshBeginPlanV1::Prepared recovery; commit_session_refresh_begin_batch_result replays the same begin in the committing transaction and persists the first batch only when the replayed binding still matches it. On divergence or batch refusal the begin commits alone (BeganOnly), leaving the durable running operation a crash between the two old commits would already have left. The shared cursor key is provisioned in its own transaction before planning (a rolled-back mint would desynchronize the two replays; once a key exists the provision commit appends no WAL frames), and SessionRefreshRecoveryV1 now carries accepted_at so the operation's created_at stays ordered before progress.recorded_at.
  • crates/tracedecay-session-temporal-store/src/handle.rs (+ tracedecay-global-db, test_registered_impls.rs): SessionTemporalWriteTxn::rollback so 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 keeps Fail/Deferred/error accounting identical to the durable paths.
  • Tests: session_store_read_cost boundary expectation (4,3,4) → (4,3,3); temporal_refresh suite tail asserts 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: 8c1d347

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