Skip to content

fix(acp)!: preserve connection lifetimes and graceful drain - #385

Merged
benbrandt merged 3 commits into
mainfrom
work/split/connection-lifetimes
Oct 2, 2026
Merged

benbrandt merged 3 commits into
mainfrom
work/split/connection-lifetimes

Conversation

@benbrandt

Copy link
Copy Markdown
Member

Summary

Make connection completion explicit so wrappers do not mistake an endpoint with no owned task for a finished component, hang on unrelated open input, or discard accepted final output.

This is the connection-lifetime slice only. It does not change ACP/MCP wire schemas, dependency pins, raw Channel sender/receiver types, or introduce backpressure/resource limits.

API and migration

  • Breaking: ConnectTo::into_channel_and_future now returns (Channel, Option<ConnectionDriver>) instead of (Channel, BoxFuture<Result<()>>). None means an endpoint with no owned work—not EOF and not a ready-success future.
  • ConnectionDriver::new(future) owns opaque work. Finite foreground completion does not indefinitely join arbitrary opaque tasks after protocol handoff.
  • ConnectionDriver::with_finish(future, hook) lets built-in and custom adapters declare cooperative finish. The nonblocking one-shot hook requests stop/seal/drain; the still-polled future proves physical flush/write-half-close and reports errors. No implicit timeout is added.
  • request_finish() is idempotent: true means supported and requested, including an earlier request; false means opaque. The callback runs at most once. Already-requested work retains its graceful contract across handoff.
  • map_future supports tracing, error annotation, and completion cleanup without losing finish capability or requested state. Transparent wrappers preserve the original optional driver unchanged.
  • Passive endpoints have no finish hook. Dropping a driver drops its owned future without requesting graceful completion; dropping only the hook does not invoke it or necessarily stop that work.

Most components implementing only connect_to keep the default conversion. Custom conversion overrides and low-level callers must handle absence explicitly. Buffered custom transports that need finite physical-drain guarantees expose the cooperative hook and coordinate it in both direct and normalized entry points.

Migration guide · Public driver API

Lifecycle fixes

  • Preserve passive channel half-closes so write EOF can be followed by a final reverse response.
  • Treat successful owned completion as authoritative even when sender clones escape; reject new output and drain frames already accepted.
  • Drain routable builder output after successful finite foreground completion. Fail unresolved readiness-gated requests instead of publishing them after shutdown. Physical progress continues without starting unrelated queued application tasks solely for sink drain.
  • Stop new application delivery after foreground success, preserve close callbacks already underway, and discard irrelevant late/queued input during physical drain without hiding genuine I/O errors.
  • Explicitly flush and close physical writers, including read/write halves sharing one socket or duplex stream; do not rely only on dropping the writer.
  • Apply one ownership-aware rule across protocol connectors, agent/proxy routers, and initialization rejection. Await cooperative drain, check ready errors before cancelling opaque work, and do not require independently open remote input to reach EOF.
  • Retain cancellation ownership when HTTP natural cleanup has taken the router join handle, so explicit shutdown cannot detach a blocked router. Preserve the no-driver HTTP pump lifetime.

Regression evidence

Retained tests cover the previously reproduced failures: direct builder output truncation, escaped-producer completion hangs, split-stream write EOF, late input cancelling gated flush, and HTTP shutdown during router drain. Public custom-adapter coverage adds one-byte physical write backpressure, a separate flush gate, exact flush-error propagation, direct/normalized/decorated entry points, already-requested handoff, opaque cancellation, and one-shot/drop behavior.

Review order:

  1. Driver API and contracts
  2. Builder/transport coordination and physical writer
  3. Protocol-peer consumers and HTTP connection ownership
  4. Normalization regressions, public composition tests, and protocol-router finish tests

Validation

Passed locally on the committed source, with incremental compilation/debug info disabled and build warnings denied:

  • Full unfiltered just test, including doctests and compile-fail coverage
  • Strict workspace/all-targets/all-features Clippy (-D warnings)
  • All eight CI-equivalent feature-powerset partitions
  • Rust 1.88 workspace/all-targets/all-features check, plus minimal/all-feature WebAssembly configurations
  • Strict Clippy for wasm32-wasip1, wasm32-wasip2, and wasm32-unknown-unknown
  • docs.rs-style nightly documentation and stable WASI documentation
  • Formatting, typos, mdbook build, and whitespace checks

No manifest, lockfile, or CI workflow changes. Released changelog history is preserved.

Draft pending GitHub CI on this branch and final human review. Nothing has been merged or released.

Preserve passive half-closes and poll owned work alongside frame forwarding. Drain accepted output before completion using private built-in transport finish coordination, and update protocol and HTTP consumers to preserve driver identity. Reject escaped output producers on active completion and drain the installed HTTP router before unregistering. Add lifecycle regressions, migration guidance, and focused changelog entries without resource-policy changes.

BREAKING CHANGE: ConnectTo::into_channel_and_future now returns (Channel, ConnectionDriver) instead of a channel and boxed future. Custom overrides must mark owned versus passive work; existing raw Channel APIs remain unchanged.
Return None for endpoints without owned work and Some(ConnectionDriver) for genuine futures. Remove passive readiness and is_passive, keeping private finish coordination only on owned drivers. Propagate optional ownership through consumers and preserve all lifetime guarantees. Add compile-fail and absent-work half-close regressions and revise migration guidance.

BREAKING CHANGE: ConnectTo::into_channel_and_future now returns (Channel, Option<ConnectionDriver>). Low-level callers must handle absence explicitly instead of awaiting a no-op passive driver.
Expose cooperative finish requests, preserve their contract after an earlier request or future decoration, and give wrappers a map_future path. Apply one drain rule to builder and protocol-router entry points, keep physical write-half shutdown and late-input error handling, and retain HTTP router cancellation ownership. Add public custom-adapter, direct/normalized, and gated-drain regressions and migration guidance.
@benbrandt
benbrandt marked this pull request as ready for review October 2, 2026 14:56
@benbrandt
benbrandt enabled auto-merge (squash) October 2, 2026 14:56
@benbrandt
benbrandt merged commit 0b2f47e into main Oct 2, 2026
14 checks passed
@benbrandt
benbrandt deleted the work/split/connection-lifetimes branch October 2, 2026 15:02
@acp-release-bot acp-release-bot Bot mentioned this pull request Oct 1, 2026
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