Skip to content

Drain buffered coalesced values before completion - #8247

Merged
lawrencecchen merged 2 commits into
mainfrom
feat-fix-combine-bootstrap-demand
Jul 16, 2026
Merged

lawrencecchen merged 2 commits into
mainfrom
feat-fix-combine-bootstrap-demand

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #8236.

That PR restored downstream demand accounting, but completion can still overtake a value buffered before completion. This happens when downstream delivery causes a reentrant value and completion, or when the final value arrives while downstream demand is zero.

The operator now retains pending completion until its buffered value drains. It prevents reentrant delivery with one drain loop, forwards the value once demand resumes, then forwards completion exactly once.

Regression structure:

  1. aeb9aba971 adds unlimited-demand reentrancy and finite-demand final-value tests.
  2. 0bf8747f56 adds the completion-aware drain loop and makes them pass.

Prior tagged build of the full demand and completion implementation: code2, https://github.com/manaflow-ai/cmux/actions/runs/29474218386

No user-facing strings changed, so localization files are unaffected.

Summary by CodeRabbit

  • Bug Fixes
    • Updated coalesceLatest to strictly honor downstream demand before emitting buffered values.
    • Fixed ordering issues involving reentrant upstream emissions, ensuring completion can’t be observed before buffered latest values are delivered when demand allows.
    • Improved cancellation behavior to clear pending buffered state and reset demand tracking.
  • Tests
    • Added regression coverage for reentrant upstream emissions with both unlimited-demand and demand-resume scenarios.

@cursor

cursor Bot commented Jul 16, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

coalesceLatest now respects downstream demand by buffering replayed and incoming values, draining them only after requests, and delaying completion until buffered values are delivered. Regression tests cover reentrant emissions with constrained and unlimited demand.

Changes

Demand-aware coalescing

Layer / File(s) Summary
Demand-gated publisher delivery
Sources/CoalesceLatestPublisher.swift
coalesceLatest tracks downstream demand, buffers the latest value, gates replay and incoming delivery, delays completion until buffered values drain, and resets state on cancellation.
Reentrant demand regression coverage
cmuxTests/WorkspaceSidebarObservationTests.swift
DemandControlledSubscriber forwards explicit requests and verifies reentrant delivery, demand-resumed buffering, and single completion recording.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Upstream
  participant CoalesceLatestInner
  participant DemandControlledSubscriber
  Upstream->>CoalesceLatestInner: send latest value
  CoalesceLatestInner->>CoalesceLatestInner: buffer readyValue
  DemandControlledSubscriber->>CoalesceLatestInner: request demand
  CoalesceLatestInner->>DemandControlledSubscriber: deliver buffered value
  Upstream->>CoalesceLatestInner: complete
  CoalesceLatestInner->>DemandControlledSubscriber: forward completion after draining
Loading

Possibly related PRs

Suggested reviewers: azooz2003-bit, austinywang


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Package Boundaries ❌ Error CoalesceLatestPublisher.swift is built into app target cmux; it’s a generic, testable Combine operator used by sidebar observation, so it belongs behind a SwiftPM package boundary. Extract Publisher.coalesceLatest/CoalesceLatestPublisher into a small package target (e.g. CmuxSidebar or a new CmuxCombineSupport) and import it from the app target.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (23 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Changed code is a private Combine operator plus MainActor tests; no new MainActor-coupled models, Sendable mutable references, or service-protocol isolation issues were introduced.
Cmux Swift Blocking Runtime ✅ Passed No new semaphores, sleeps, locks, or sync waits were introduced; production code only uses scheduler-based coalescing, and timing scaffolding is test-only.
Cmux Browser Automation Off-Main ✅ Passed The diff only changes Sources/CoalesceLatestPublisher.swift; no browser.* automation, router, or policy-test files in scope were touched.
Cmux Expensive Synchronous Load ✅ Passed Patch only rewrites coalesceLatest; no agent-history loader, JSON/transcript scan, or other expensive sync load was added to a main/interactive path.
Cmux Cache Substitution Correctness ✅ Passed The change only adjusts a transient sidebar observation publisher’s demand handling; it doesn’t replace an authoritative read in any persistence/history/undo/snapshot path.
Cmux No Hacky Sleeps ✅ Passed Diff is Swift-only; the runtime-no-hacky-sleeps rule excludes Swift, and the added hunks contain no sleeps/timers/polling.
Cmux Algorithmic Complexity ✅ Passed The production change is an O(1) demand/state-machine update with no nested scans, sorting, or per-target rescans; added tests are fixed-size scaffolding.
Cmux Swift Concurrency ✅ Passed The diff only rewrites an existing Combine operator and adds XCTest coverage; it introduces no DispatchQueue, completion-handler, or fire-and-forget Task patterns.
Cmux Swift @Concurrent ✅ Passed The only changed file is a synchronous Combine operator; no @concurrent/nonisolated async annotations or UI-isolated async heavy work were introduced.
Cmux Swiftpm Lockfiles ✅ Passed PR diff only changes two Swift source/test files; no Package.swift, Package.resolved, .gitignore, or Xcode project files were touched.
Cmux Swift Logging ✅ Passed The diff adds no print/debugPrint/dump/NSLog/Logger/os_log or ad hoc diagnostics; only operator logic and tests were changed.
Cmux User-Facing Error Privacy ✅ Passed The PR only changes a publisher implementation and tests; no user-facing errors, alerts, API error bodies, or sensitive details were added.
Cmux Full Internationalization ✅ Passed Only Combine logic and tests changed; no user-facing Swift/UI/web text or locale catalogs were added or modified.
Cmux Swiftui State Layout ✅ Passed PASS: the patch only rewrites a Combine publisher and adds test helpers; no SwiftUI views, ObservableObject/@published, GeometryReader, lazy rows, or render-time state writes appear.
Cmux Architecture Rethink ✅ Passed The diff is a local Combine operator fix with explicit demand/completion invariants; no sleeps, locks, observers, or split ownership in production code, and test sync is test-only.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: Diff only touches coalesceLatest and tests; no NSWindow/NSPanel/WindowGroup code or cmuxAuxiliaryWindowIdentifiers changes, so the shortcut rule isn’t implicated.
Cmux Source Artifacts ✅ Passed Only Sources/CoalesceLatestPublisher.swift changed, and the diff is a hand-written source edit, not a generated artifact or scratch output.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Changed production code only rewrites demand handling; no DEBUG/test-hook seam or widened accessor was added, and new test helpers live under Tests/.
Cmux No Ambient Global State ✅ Passed Diff only changes CoalesceLatestInner internals; no new file-scope funcs, mutable globals, or singletons were added.
Title check ✅ Passed The title is concise and accurately captures the main behavior change in the pull request.
Description check ✅ Passed The description explains what changed, why, and how it was verified, though it doesn't follow the template sections exactly.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-fix-combine-bootstrap-demand

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.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b271e51381

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/CoalesceLatestPublisher.swift Outdated
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@codex review

@lawrencecchen
lawrencecchen force-pushed the feat-fix-combine-bootstrap-demand branch from 98e76f4 to acc82f4 Compare July 16, 2026 05:36

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/WorkspaceSidebarObservationTests.swift`:
- Around line 257-281: Add a regression test alongside
coalesceLatestDrainsReentrantValueBeforeCompletionWithUnlimitedDemand that uses
finite demand, sends a reentrant value followed by completion when demand is
exhausted, verifies the value and completion are buffered, then requests
additional demand and asserts the value drains before exactly one completion.

In `@Sources/CoalesceLatestPublisher.swift`:
- Around line 187-202: Update finishPendingCompletionIfPossible() so it returns
while readyValue is non-nil, retaining both the buffered value and
pendingCompletion until demand allows the value to be delivered. Remove the
logic that clears readyValue and completes immediately with zero demand; forward
completion only after the buffered value has been drained, while preserving the
existing cancellation and downstream delivery behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8cb9dfff-5311-49ea-a294-5a3da9e323d1

📥 Commits

Reviewing files that changed from the base of the PR and between b271e51 and 98e76f4.

📒 Files selected for processing (2)
  • Sources/CoalesceLatestPublisher.swift
  • cmuxTests/WorkspaceSidebarObservationTests.swift

Comment thread cmuxTests/WorkspaceSidebarObservationTests.swift Outdated
Comment thread Sources/CoalesceLatestPublisher.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: acc82f4050

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if let value = pendingValue {
pendingValue = nil
_ = downstream.receive(value)
enqueueForDelivery(value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the reentrant leading value at completion

With unlimited demand, if a downstream receiving the replay reentrantly sends a leading value, then another value inside that new window, then completes (send(2); send(3); send(completion:)), this line enqueues the pending trailing value through readyValue = value while isDelivering is still true, overwriting the held leading value before the outer drain can emit it. Fresh evidence beyond the prior fixed thread is the extra in-window value before completion; the old sink behavior would emit [1, 2, 3], but this change emits [1, 3], so leading-edge semantics are still broken for completing reentrant streams.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a deterministic crash introduced by a prior commit where coalesceLatest requested .unlimited upstream demand but ignored downstream demand, causing a reentrant workspace emission to reach AsyncPublisher after its one-element budget was exhausted and trap at the Combine Publisher+AsyncSequence.swift:112 assertion.

  • Demand accounting: Replaces the old demand/undeliveredValue fields with downstreamDemand, readyValue, isDelivering, and pendingCompletion. The drain loop in drainReadyValue() gates all delivery on downstreamDemand > .none and uses isDelivering to prevent reentrant double-delivery while still capturing values produced mid-delivery.
  • Completion ordering: receive(completion:) stores the completion in pendingCompletion and defers forwarding until finishPendingCompletionIfPossible() confirms readyValue == nil, so a buffered value always precedes the terminal event regardless of when demand arrives.
  • Regression tests: Two new DemandControlledSubscriber-based tests cover (1) the reentrant unlimited-demand path and (2) the demand-limited path where the subscriber does not request before the upstream finishes.

Confidence Score: 5/5

Safe to merge; the fix is narrowly scoped to the coalescing operator's demand accounting and completion ordering, and both new regression tests pass against the corrected implementation.

The drain loop, reentrancy guard, and deferred-completion logic all trace correctly for unlimited-demand and demand-limited subscribers, including reentrant upstream emissions mid-delivery. No blocking primitives, actor isolation issues, test seams in production source, or other rule violations were found.

No files require special attention.

Important Files Changed

Filename Overview
Sources/CoalesceLatestPublisher.swift Core fix: replaces the old single-shot demand/undeliveredValue model with a proper drain loop guarded by isDelivering, accumulates demand in downstreamDemand, and defers completion via pendingCompletion until readyValue is drained. Logic traces cleanly for both unlimited and demand-limited subscribers, including reentrant upstream emissions during delivery.
cmuxTests/WorkspaceSidebarObservationTests.swift Adds DemandControlledSubscriber helper and two regression tests that exercise the demand-limited buffered-value-before-completion path and the reentrant unlimited-demand drain path; both tests are in the test target and contain no production seams.

Reviews (2): Last reviewed commit: "fix(sidebar): drain reentrant values bef..." | Re-trigger Greptile

Comment on lines +187 to 202
private func finishPendingCompletionIfPossible() {
guard !isCancelled,
!isDelivering,
let completion = pendingCompletion
else { return }

pendingCompletion = nil
// Combine completion is not demand-gated. Values that reentered an
// active delivery drain first while demand remains; if demand is zero,
// the buffered latest value cannot legally precede completion and drops.
readyValue = nil
downstreamDemand = .none
isCancelled = true
downstream.receive(completion: completion)
upstreamSubscription = nil
}

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.

P2 Silent value drop when demand is zero at completion

finishPendingCompletionIfPossible unconditionally clears readyValue before forwarding completion. In the unlimited-demand path this is always nil by the time the drain exits, so there is no loss. For a demand-limited subscriber, however, if the upstream completes while downstreamDemand == .none and a value is sitting in readyValue (e.g. the leading replay was delivered but the subscriber has not yet called request again), that buffered value is silently dropped — the subscriber receives completion without ever seeing the latest coalesced value.

For the Publisher.values / AsyncSequence bridge motivating this fix, next() always requests .max(1) before suspending, so demand is always in-flight and this case cannot arise in practice. Worth noting here for future demand-limited consumers that are slow to re-request: the contract is "latest value only when demand exists", and a completion that races a pending request wins.

@lawrencecchen
lawrencecchen force-pushed the feat-fix-combine-bootstrap-demand branch from acc82f4 to 0bf8747 Compare July 16, 2026 05:52
@cursor

cursor Bot commented Jul 16, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@codex review

@lawrencecchen lawrencecchen changed the title Honor Combine demand during sidebar coalescing Drain buffered coalesced values before completion Jul 16, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0bf8747f56

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

private func enqueueForDelivery(_ value: Input) {
readyValue = value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't overwrite demanded reentrant emissions

When downstream still has outstanding demand but enqueueForDelivery is re-entered before the current downstream.receive returns, this single slot replaces an earlier ready emission. For example, with .coalesceLatest(for: .zero, ...) (or a callback that stalls past the interval), an unlimited-demand sink that sends 2 and then 3 from its receive(1) only receives [1, 3]; neither value is inside a coalescing window and the old unlimited-demand behavior delivered both. Keep a FIFO of ready emission points, or otherwise avoid conflating values while demand remains.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen merged commit 7d62e38 into main Jul 16, 2026
7 checks passed
@lawrencecchen
lawrencecchen deleted the feat-fix-combine-bootstrap-demand branch July 16, 2026 09:28
@lawrencecchen
lawrencecchen restored the feat-fix-combine-bootstrap-demand branch July 18, 2026 10:24
bn-l pushed a commit to bn-l/cmux that referenced this pull request Oct 4, 2026
Ported from upstream manaflow-ai#8247 (7d62e38): "Drain buffered coalesced values before completion (manaflow-ai#8247)".
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.

1 participant