Skip to content

Commit 0dacbcd

Browse files
jmoseleyCopilotSteveSandersonMS
authored
[Rust] Add PreparedSession for loss-free startup event subscription (#2319)
* [Rust] Add PreparedSession for loss-free startup event subscription `Session::subscribe()` can only be called once the session handle exists, so every event the runtime broadcast during `session.create` / `session.resume` had no receiver installed and was dropped. Ephemeral events like `session.idle` are never written to the session log, so `get_messages` cannot recover them afterwards either. `Client::prepare_session` / `prepare_resume_session` return a `PreparedSession` that owns the session's broadcast channel up front: subscribe first, then `start()`. `prepare_*` is synchronous and inert — it validates the event buffer capacity, allocates a local channel and cancellation token, and performs no router registration, task spawn, or wire activity until `start()` is first polled. `start(self)` consumes the handle and the type is deliberately not `Clone`, so a prepared session can never produce two event loops. `create_session` / `resume_session` become wrappers over `prepare_*(config)?.start().await`, preserving their RPC sequences and error kinds. Their bodies moved into private start paths that take the sender and token by injection rather than allocating their own. Both configs gain a runtime-only `event_buffer_capacity` (default 512, `Some(0)` rejected as `InvalidConfig`, never clamped). The buffer is finite, so slow subscribers observe `Lagged` instead of applying backpressure. Cancellation cleanup is now symmetric. `PendingSessionRegistration` grew a deferred variant that resolves the session ID from the inline-response stash, so the create path — including the cloud server-assigned-ID path, which previously had no RAII guard at all — unregisters and cancels when the startup future is dropped or fails. Registration and stashing now happen under one lock hold to close the window where a concurrent drop would miss a just-registered session. The mcp-auth-interest error path on both create and resume now cancels and awaits the event loop instead of returning through `?`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * [Rust] Close two session-registration cancellation races Follow-up to the `PreparedSession` change. Two cancellation races remained in session registration, both reachable from a caller simply dropping a `start()` future. **Deferred cloud-create registration.** For a cloud session with no caller-pinned ID the CLI assigns the ID, so the SDK can only register on the notification router from the inline `session.create` response callback. The read loop removes the pending-response entry *before* invoking that callback, so a startup future dropped in that window found an empty stash, cleaned up nothing, and the callback then registered a session with no owner — a permanent router leak. Registration state now lives in a shared `DeferredRegistration` slot (`Pending` / `Registered` / `Cancelled` / `Claimed`) that the callback, the startup path, and the cancellation guard all arbitrate through. The callback registers *under the slot lock*, so registering and publishing ownership are atomic with respect to cancellation: a concurrent guard either wins and marks the slot `Cancelled`, in which case the callback registers nothing, or it loses and finds a `Registered` slot to tear down. Never both, and never neither. The pinned-ID path uses the same slot, pre-populated, so create has one cleanup mechanism instead of two. **Stale cleanup versus a same-ID retry.** Unregistering by session ID alone removed whichever registration happened to hold the ID. Because cleanup of an abandoned startup is signalled rather than awaited, a caller that aborted a startup and immediately retried with the same pinned ID could have the retry's registration evicted by the dead attempt, silently stranding the live session with no event routing. The same applied to a `Session` dropped after being superseded. Registrations now carry a `RegistrationToken` identity and removal is a compare-and-remove: an owner removes only the exact registration it registered. Applied to create, resume, `Session::disconnect`, and `Session::drop`. `Client::stop` and `cleanup_sessions_for_test` keep removing unconditionally — they tear down every session and the runtime regardless of owner. Tests gate both windows deterministically rather than by timing. The slot state machine is driven directly at the exact interleaving the read loop creates, in both orders, and the router's compare-and-remove is covered on its own. End to end: a cancelled cloud create leaves no registration, no subscription, and no task behind whether cancellation lands before or after the callback registered, and a same-ID retry still succeeds; and create, resume, and `Session` drop each survive a stale owner's cleanup running after a retry has taken over the ID. Each test was confirmed to fail against a mutated implementation. No public API change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Rust] Gate registered_session_ids to test configurations `Client::registered_session_ids` has no caller in a default-feature build: the in-crate unit tests reach it under `cfg(test)`, and the public `registered_session_ids_for_test` wrapper is gated on `feature = "test-support"`. A plain `cargo build` or `cargo clippy` therefore warned `dead_code` for it. Gate the method on `any(test, feature = "test-support")`, matching the convention already used for the other test-only helpers in this file. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Rust] Narrow the PreparedSession guarantee to routed events `Client::prepare_session` promised consumers would observe "every event a session emits". That is broader than the implementation for cloud creates with a server-assigned ID: the SDK cannot register the session on its notification router until the `session.create` response arrives, so notifications emitted before that point are not routable to any session and never reach a subscriber. Qualify the primary API documentation and the changelog as *routed* events, and point callers at pinning `SessionConfig::session_id` for complete pre-response coverage. `PreparedSession`'s type-level docs, `rust/README.md`, and `docs/features/streaming-events.md` already documented this limitation; the entry-point docs now match them. Documentation only: no API, behavior, or wire change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Docs] Unwrap the lone Rust tab in the streaming-events article The "Subscribing before a session starts" section has only a Rust example, and the docs normalization pipeline converts a `<details>` group into a tabbed language switcher only when two or more consecutive blocks are present. A single block renders as raw collapsible HTML on docs.github.com. Drop the `<details>`/`<summary>` wrapper and leave the code fence directly in the article, matching the repository docs style guide. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Rust] Keep session IDs out of prepared-session test diagnostics Two polling helpers formatted session identifiers into their failure messages: `await_no_registrations` rendered the router's registered ID list with `{:?}`, and `await_registered` interpolated the awaited ID. A downstream consumer that vendors this crate has CodeQL rules flagging identifiers reaching formatted output, so both were reported there even though the SDK's own analysis was clean. Report an outstanding-registration count and a static expectation message instead. Both helpers keep their exact predicates and deadline behavior: `await_no_registrations` still returns only when the router holds zero registrations, and `await_registered` still blocks on the exact ID it was given, so no assertion is weakened. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Rust] Cover the MCP-auth interest failure path on create and resume `session.eventLog.registerInterest` is the last fallible step of startup when an MCP-auth handler is installed, and its failure branch was untested: the existing MCP-auth tests only return successful interest responses, and the prepared-session failure tests covered create RPC errors and ID mismatches only. Add a failing-interest test for each of create and resume, asserting the same contract the sibling failure tests assert: the original error kind reaches the caller, the router registration is gone, and a subscription installed before `start()` is closed. The resume test also asserts the best-effort `session.skills.reload` is never issued, since interest registration runs ahead of it. Verified by mutation that both tests execute the branch. Removing the `registration.cleanup(event_loop)` call does not turn them red, because `PendingSessionRegistration::drop` cancels and releases the same registration synchronously — the explicit cleanup is defense in depth on this path, and the tests assert the observable contract rather than which of the two mechanisms performed it. Test-only change: no API, behavior, or wire impact. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Rust] Reconcile the subscribe docs with the buffering contract `PreparedSession::subscribe` promised that "every subscriber receives every event", which contradicts the paragraph directly above it: the broadcast buffer is finite, so a subscriber that falls behind the configured capacity observes `Lagged` and skips events instead of applying backpressure. Say that explicitly and link the `Lagged` type. Also replace "should" with "must" where the streaming-events article and the changelog describe what a consumer needing lossless startup delivery has to do. The docs style guide reserves ambiguous modals for optional actions, and `rust/README.md` already phrased this as "must". Documentation only: no API, behavior, or wire change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6 * [Rust] Reconcile PreparedSession with current main Preserve current router registration ownership and feature configuration while rebasing the focused startup subscription change.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [Rust] Format rebased session startup code Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [Rust] Avoid exposing session IDs in diagnostics Use a count-only test helper for the polling diagnostic so CodeQL does not treat session identifiers as logged data.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [Rust] Keep session IDs out of test diagnostics CodeQL flagged the registration polling helper because the count was derived from a Vec<SessionId>. Count registrations directly on the router instead, and assert on registration identity without formatting IDs into failure messages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com> Copilot-Session: 0f9d5ac7-9999-4f37-82b3-a5533bfbc1f6
1 parent 8f14e9a commit 0dacbcd

9 files changed

Lines changed: 1996 additions & 62 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,28 @@ const session = await joinSession({
4444
const token = process.env.GITHUB_TOKEN;
4545
```
4646

47+
### Feature: early session-event subscription (Rust)
48+
49+
The Rust SDK can now observe every event routed to a session, starting with that session's very first routed event. `Client::prepare_session` and `Client::prepare_resume_session` return an inert `PreparedSession` that owns the session's event channel, so a subscription can be installed *before* any protocol activity begins:
50+
51+
```rust
52+
let prepared = client.prepare_session(
53+
SessionConfig::default().with_event_buffer_capacity(2048),
54+
)?;
55+
let mut events = prepared.subscribe();
56+
let session = prepared.start().await?;
57+
```
58+
59+
Previously, `Session::subscribe` could only be called on the returned session, so events the runtime emitted while `session.create` / `session.resume` was still in flight were broadcast with no receiver installed and dropped. Ephemeral events such as `session.idle` are not persisted, so they could not be recovered with `getMessages` either.
60+
61+
The guarantee is scoped to *routed* events. For cloud sessions where the server assigns the session ID, the SDK cannot route notifications until the `session.create` response arrives and the ID is known, so events emitted before that point are not routable to any session. Pin `session_id` on the config to get router registration before the RPC, and with it complete pre-response coverage.
62+
63+
`prepare_*` is synchronous and inert: it validates the buffer capacity and allocates a local channel, and performs no router registration, task spawn, or wire activity until `start()` is first polled. `start(self)` consumes the handle and `PreparedSession` is not `Clone`, so a prepared session can never produce two event loops. Dropping an unstarted handle leaves no state behind; dropping a polled `start()` future cancels the startup and unregisters the session, so a retry with the same session ID succeeds. Session registrations now carry an ownership identity, so cleanup removes only the exact registration it owns and an abandoned startup can never evict a same-ID retry (or a session that replaced it).
64+
65+
Both `SessionConfig` and `ResumeSessionConfig` gained a runtime-only `event_buffer_capacity` option (default 512, `Some(0)` rejected as an invalid config). The buffer is finite, so slow subscribers observe `Lagged` rather than applying backpressure; consumers that need a lossless view of a large startup burst must size the buffer accordingly or drain concurrently with `start()`.
66+
67+
`create_session` and `resume_session` are unchanged wrappers over `prepare_*(...)?.start()` with identical RPC sequences and error kinds.
68+
4769
### Feature: host-injected managed settings permissions
4870

4971
Session create and resume accept a new optional `managedSettings` option that injects an enterprise permissions policy at session startup, alongside the existing `enableManagedSettings` self-fetch flag. The current contract is permissions-only: `disableBypassPermissionsMode` (the literal `"disable"`), plus `deny`, `ask`, and `allow` rule lists. The layer composes restrictively with any server- or device-level managed settings (deny/ask are unioned, every present allow list must admit a tool, and `disableBypassPermissionsMode` is deny-wins).

docs/features/streaming-events.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,48 @@ session.on(AssistantMessageDeltaEvent.class, event ->
218218
> [!TIP]
219219
> **(TypeScript)** The TypeScript SDK uses a discriminated union—when you match on `event.type`, the `data` payload is automatically narrowed to the correct shape.
220220
221+
## Subscribing before a session starts
222+
223+
A session can emit events before its create or resume call returns. The agent may already be working—especially on resume with `continuePendingWork`—and ephemeral events such as `session.idle` are never written to the session log, so `getMessages` cannot recover them afterwards. A subscription installed after the session handle exists misses that startup window.
224+
225+
> [!TIP]
226+
> **(Rust)** `Client::prepare_session` and `Client::prepare_resume_session` return a `PreparedSession` that owns the session's event channel before any protocol activity happens. Subscribe first, then call `start()`.
227+
228+
```rust
229+
use github_copilot_sdk::{Client, SessionConfig};
230+
231+
async fn create_without_missing_startup_events(
232+
client: &Client,
233+
) -> Result<(), github_copilot_sdk::Error> {
234+
let prepared = client.prepare_session(
235+
SessionConfig::default().with_event_buffer_capacity(2048),
236+
)?;
237+
238+
// Installed before any wire activity: nothing is dropped for lack of a receiver.
239+
let mut events = prepared.subscribe();
240+
tokio::spawn(async move {
241+
while let Ok(event) = events.recv().await {
242+
println!("{}", event.event_type);
243+
}
244+
});
245+
246+
let session = prepared.start().await?;
247+
let _ = session;
248+
Ok(())
249+
}
250+
```
251+
252+
`prepare_*` is synchronous and inert: it validates the buffer capacity, allocates a local channel, and does nothing else. No session is registered and nothing reaches the CLI until `start()` is first polled. Dropping a prepared session that was never started leaves no state behind and closes its subscriptions; dropping the `start()` future cancels the in-flight startup and unregisters the session, so a retry with the same session ID succeeds. Cleanup is scoped to the exact registration the abandoned startup owned, so it cannot evict a retry that has already taken over the same session ID.
253+
254+
Startup buffering is worth planning for:
255+
256+
* The event buffer is finite—512 events unless `event_buffer_capacity` overrides it. A capacity of `0` is rejected with an invalid-config error rather than clamped.
257+
* Slow subscribers observe a `Lagged` error reporting how many events were skipped. They never apply backpressure to the session's event loop.
258+
* Consumers that need a lossless view of a large startup burst must either configure a capacity that covers it or drain the subscription concurrently with `start()`.
259+
260+
> [!NOTE]
261+
> For cloud sessions where the server assigns the session ID, the SDK cannot route notifications until the create response arrives and the ID is known. Events emitted before that point are not routable to any session. The guarantee is narrower: routed events are never dropped for lack of an installed receiver. Pin `session_id` on the config to get routing—and full pre-response coverage—from the first byte.
262+
221263
## Render only the parent agent response
222264

223265
Sub-agent events share the parent session stream and include envelope-level `agentId`. Root/main agent events and session-level events omit `agentId`, so main-chat renderers can ignore assistant events where `agentId` is set and route those events to traces or progress UI instead.

rust/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ required-features = ["test-support"]
118118
test = false
119119
bench = false
120120

121+
[[test]]
122+
name = "prepared_session_test"
123+
required-features = ["test-support"]
121124
[build-dependencies]
122125
dirs = "5"
123126
flate2 = "1"

rust/README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,36 @@ while let Ok(event) = events.recv().await {
717717

718718
When streaming is off (the default), only the final `assistant.message` and `assistant.reasoning` events fire. Delta events arrive in order; concatenating their `delta` text payloads reproduces the final message.
719719

720+
#### Subscribing before the session starts
721+
722+
`session.subscribe()` can only be called once the session exists, so any event the runtime emits while `session.create` / `session.resume` is still in flight is broadcast with no receiver installed and is not delivered. Ephemeral events such as `session.idle` are not written to the session log either, so `get_messages` can't recover them afterwards.
723+
724+
`Client::prepare_session` / `Client::prepare_resume_session` close that window. They return a `PreparedSession` that owns the session's broadcast channel up front:
725+
726+
```rust,ignore
727+
let prepared = client.prepare_session(
728+
SessionConfig::default().with_event_buffer_capacity(2048),
729+
)?;
730+
731+
// Installed before any wire activity happens.
732+
let mut events = prepared.subscribe();
733+
tokio::spawn(async move {
734+
while let Ok(event) = events.recv().await {
735+
println!("{}", event.event_type);
736+
}
737+
});
738+
739+
let session = prepared.start().await?;
740+
```
741+
742+
`prepare_*` is synchronous and inert — it validates the buffer capacity, allocates a local channel and cancellation token, and touches neither the router nor the transport until `start()` is first polled. `start(self)` consumes the handle and `PreparedSession` is deliberately not `Clone`, so a prepared session can never spawn two event loops. Dropping an unstarted handle leaves no state and closes its subscriptions; dropping the `start()` future cancels the startup, unregisters the session, and lets a same-ID retry succeed. Cleanup removes only the exact registration that startup owned, so a retry started while an abandoned attempt is still unwinding is never evicted by it.
743+
744+
The buffer is finite — `session::DEFAULT_EVENT_BUFFER_CAPACITY` (512) unless `event_buffer_capacity` overrides it, and `Some(0)` is rejected as `ErrorKind::InvalidConfig` rather than clamped. Subscribers that fall behind observe `RecvErrorKind::Lagged` with the skipped count instead of applying backpressure, so a consumer that needs a lossless view of a large startup burst must configure enough capacity or drain concurrently with `start()`.
745+
746+
For cloud sessions where the server assigns the session ID, notifications can't be routed until the create response arrives; the guarantee is that *routed* events are never dropped for lack of a receiver. Pin `session_id` for full pre-response coverage.
747+
748+
`create_session` / `resume_session` are unchanged wrappers over `prepare_*(...)?.start()`, with identical RPC sequences and error kinds.
749+
720750
### Infinite Sessions
721751

722752
Enable the SDK's session-store integration so conversations persist across CLI restarts and grow beyond the model's context window via automatic compaction:
@@ -920,14 +950,20 @@ none of them are scheduled for removal.
920950
arg vectors for "prepend before subcommand" vs "append after the
921951
built-in flags", giving precise control over CLI invocation order
922952
without string-splicing.
953+
- **`Client::prepare_session` / `prepare_resume_session`** — return an inert
954+
`PreparedSession` whose `subscribe()` installs an event receiver before any
955+
protocol activity, so startup events (including ephemeral `session.idle`)
956+
aren't dropped. Other SDKs register callbacks on a config object instead,
957+
which sidesteps the problem in a way Rust's broadcast-based `subscribe()`
958+
cannot.
923959

924960
## Layout
925961

926962
| File | Description |
927963
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
928964
| `lib.rs` | `Client`, `ClientOptions`, `CliProgram`, `Transport`, `Error` |
929965
| `extension_launch_provider.rs` | Connection-global `ExtensionLaunchProvider` trait and launch profile DTOs |
930-
| `session.rs` | `Session` struct, event loop, `send`/`send_and_wait`, `Client::create_session`/`resume_session` |
966+
| `session.rs` | `Session` struct, `PreparedSession`, event loop, `send`/`send_and_wait`, `Client::create_session`/`resume_session`/`prepare_session`/`prepare_resume_session` |
931967
| `subscription.rs` | `EventSubscription` / `LifecycleSubscription` (`Stream`-able observer handles for `subscribe()` / `subscribe_lifecycle()`) |
932968
| `handler.rs` | `PermissionHandler`, `ElicitationHandler`, `UserInputHandler`, `ExitPlanModeHandler`, `AutoModeSwitchHandler` traits; `ApproveAllHandler`, `DenyAllHandler` |
933969
| `hooks.rs` | `SessionHooks` trait, `HookEvent`/`HookOutput` enums, typed hook inputs/outputs |

rust/src/lib.rs

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2324,15 +2324,18 @@ impl Client {
23242324

23252325
/// Register a session to receive filtered events and requests.
23262326
///
2327-
/// Returns per-session channels for notifications and requests, routed
2328-
/// by `sessionId`. Starts the internal router on first call.
2329-
///
2330-
/// When done, call [`unregister_session`](Self::unregister_session) to
2331-
/// clean up (typically on session destroy).
2327+
/// Returns the per-session channels plus a
2328+
/// [`RegistrationToken`](crate::router::RegistrationToken) identifying
2329+
/// *this* registration. Registering an ID that is already registered
2330+
/// replaces the previous registration.
2331+
///
2332+
/// When done, call
2333+
/// [`unregister_session_owned`](Self::unregister_session_owned) with
2334+
/// that token to clean up (typically on session destroy).
23322335
pub(crate) fn register_session(
23332336
&self,
23342337
session_id: &SessionId,
2335-
) -> crate::router::SessionChannels {
2338+
) -> crate::router::SessionRegistration {
23362339
self.inner.router.ensure_started(
23372340
&self.inner.notification_tx,
23382341
&self.inner.request_rx,
@@ -2344,9 +2347,30 @@ impl Client {
23442347
self.inner.router.register(session_id)
23452348
}
23462349

2347-
/// Unregister a session, dropping its per-session channels.
2348-
pub(crate) fn unregister_session(&self, session_id: &SessionId) {
2349-
self.inner.router.unregister(session_id);
2350+
/// Unregister a session only if `token` still identifies the live
2351+
/// registration.
2352+
///
2353+
/// Session IDs can be reused: a caller may retry a cancelled startup
2354+
/// with the same pinned ID while the previous owner is still being torn
2355+
/// down. Compare-and-remove keeps a stale owner from unregistering the
2356+
/// live session that replaced it.
2357+
pub(crate) fn unregister_session_owned(
2358+
&self,
2359+
session_id: &SessionId,
2360+
token: crate::router::RegistrationToken,
2361+
) {
2362+
self.inner.router.unregister_owned(session_id, token);
2363+
}
2364+
2365+
/// Snapshot the session IDs currently registered on the router.
2366+
///
2367+
/// Crate-internal so in-crate unit tests can assert registration
2368+
/// lifecycle without depending on the `test-support` feature, which
2369+
/// only gates the equivalent *public* test helper. Compiled only for
2370+
/// those two configurations — a default-feature build has no caller.
2371+
#[cfg(any(test, feature = "test-support"))]
2372+
pub(crate) fn registered_session_ids(&self) -> Vec<SessionId> {
2373+
self.inner.router.session_ids()
23502374
}
23512375

23522376
pub(crate) fn register_github_token_provider(
@@ -2596,6 +2620,24 @@ impl Client {
25962620
);
25972621
}
25982622

2623+
#[cfg(feature = "test-support")]
2624+
#[doc(hidden)]
2625+
/// Snapshot the session IDs currently registered on this client's
2626+
/// notification router. This is test-harness plumbing, not part of the
2627+
/// supported SDK API.
2628+
pub fn registered_session_ids_for_test(&self) -> Vec<SessionId> {
2629+
self.registered_session_ids()
2630+
}
2631+
2632+
#[cfg(feature = "test-support")]
2633+
#[doc(hidden)]
2634+
/// Count the sessions currently registered on this client's notification
2635+
/// router. Deliberately never materialises the session IDs themselves so
2636+
/// they cannot leak into test diagnostics.
2637+
pub fn registered_session_count_for_test(&self) -> usize {
2638+
self.inner.router.session_count()
2639+
}
2640+
25992641
#[cfg(feature = "test-support")]
26002642
#[doc(hidden)]
26012643
/// Disconnect and delete every session owned by this test client's isolated

0 commit comments

Comments
 (0)