Skip to main content

monoloop_loop/transaction/lifecycle/
owner.rs

1//! Unique runtime owner, cloneable handle, and production start (v2 §7).
2
3use super::super::bootstrap::RuntimeBootstrap;
4use super::super::channel_registry::{ChannelBinding, LiveChannel};
5use super::super::error::StartupError;
6use super::super::host_tools::HostToolRegistry;
7use super::super::mcp::McpGatewayHandle;
8use super::super::state::RuntimeState;
9use super::admission::admit;
10use super::capacity::{ReservationPool, ReservationPoolError};
11use super::coordinator::WorkerMessage;
12use super::ledger::LifecycleLedger;
13use super::mcp_listener::DEFAULT_MCP_MAX_ROUTES;
14use super::shutdown::ShutdownTicket;
15use super::supervisor::{
16    run_supervisor, wait_until_drain_complete, ControlCommand, RuntimeShared, STATE_ACCEPTING,
17    STATE_QUIESCING, STATE_STARTING, STATE_STOPPED,
18};
19use crate::transaction::mcp::McpGateway;
20use monoloop_contracts::{
21    AdmissionError, AdmissionReceipt, ChannelId, ChannelKind, ShutdownWaitOutcome,
22    TerminationDisposition, TerminationMode, TransactionSelector, TransactionSubmitRequest,
23};
24use std::collections::HashMap;
25use std::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering};
26use std::sync::{Arc, Mutex};
27use std::thread::JoinHandle as OsJoinHandle;
28use std::time::Duration;
29use tokio::sync::{mpsc, oneshot, Notify};
30
31/// Unique owner of the executor, supervisor, ledger, connectors, and shutdown state.
32#[must_use = "RuntimeOwner must begin_shutdown and wait_stopped until Stopped"]
33pub struct RuntimeOwner {
34    shared: Arc<RuntimeShared>,
35    /// Dedicated OS thread that owns the Tokio runtime.
36    thread: Option<OsJoinHandle<()>>,
37    /// Signaled after executor `shutdown_timeout` completes (D-049).
38    thread_exited: Option<oneshot::Receiver<()>>,
39    pool: Arc<ReservationPool>,
40    /// Realized Connector instances (owner-held; handle clones the Arc for lookup).
41    channels: Arc<HashMap<ChannelId, LiveChannel>>,
42}
43
44/// Cloneable admission/control handle (no executor shutdown authority).
45#[derive(Clone)]
46pub struct TransactionRuntimeHandle {
47    shared: Arc<RuntimeShared>,
48    pool: Arc<ReservationPool>,
49    max_tools: usize,
50    /// Read-only channel map (same Arc owned by [`RuntimeOwner`]).
51    channels: Arc<HashMap<ChannelId, LiveChannel>>,
52    tools: HostToolRegistry,
53}
54
55/// Result of a successful production start handshake.
56pub struct StartedRuntime {
57    /// Unique owner.
58    pub owner: RuntimeOwner,
59    /// Cloneable control handle.
60    pub handle: TransactionRuntimeHandle,
61}
62
63impl StartedRuntime {
64    /// Production start: owns a dedicated multi-thread Tokio executor.
65    pub fn start(bootstrap: RuntimeBootstrap) -> Result<Self, StartupError> {
66        bootstrap.config.validate()?;
67
68        // §23: TransactionLimits.max_tool_schema_bytes — fail closed at start
69        // so HostToolRegistry construction with the default ceiling cannot
70        // bypass a tighter runtime limit (D-056).
71        let max_schema = bootstrap
72            .config
73            .transaction_limits
74            .max_tool_schema_bytes
75            .max(1);
76        for spec in bootstrap.tools.specs_sorted() {
77            let schema_bytes = serde_json::to_vec(spec.input_schema.as_value())
78                .map(|b| b.len())
79                .unwrap_or(usize::MAX);
80            if schema_bytes > max_schema {
81                return Err(StartupError::InvalidConfig(
82                    "tool schema exceeds max_tool_schema_bytes",
83                ));
84            }
85        }
86
87        let mut live: HashMap<ChannelId, LiveChannel> = HashMap::new();
88        let mut capacity_pairs: Vec<(ChannelId, usize)> = Vec::new();
89        for (id, binding) in bootstrap.channels.iter() {
90            binding.descriptor().validate()?;
91            let instance = binding
92                .connector_factory
93                .create()
94                .map_err(StartupError::from)?;
95            match binding.kind {
96                ChannelKind::ExternalAgent if instance.sessions.is_none() => {
97                    return Err(StartupError::SessionAdapterMismatch(
98                        "ExternalAgent requires SessionAdapter",
99                    ));
100                }
101                ChannelKind::DirectLlm if instance.sessions.is_some() => {
102                    return Err(StartupError::SessionAdapterMismatch(
103                        "DirectLlm must not carry SessionAdapter",
104                    ));
105                }
106                _ => {}
107            }
108            let channel_max = binding
109                .limits
110                .max_active_transactions
111                .min(bootstrap.config.transaction_limits.max_active_per_channel);
112            if channel_max == 0 {
113                return Err(StartupError::InvalidConfig(
114                    "channel max_active_transactions must be nonzero",
115                ));
116            }
117            capacity_pairs.push((id.clone(), channel_max));
118            live.insert(
119                id.clone(),
120                LiveChannel {
121                    binding: clone_binding(binding),
122                    instance,
123                },
124            );
125        }
126
127        let max_active = bootstrap.config.transaction_limits.max_active_transactions;
128        if max_active == 0 {
129            return Err(StartupError::InvalidConfig(
130                "max_active_transactions must be nonzero",
131            ));
132        }
133        let pool = ReservationPool::try_new(max_active, capacity_pairs).map_err(|e| match e {
134            ReservationPoolError::ZeroGlobal => {
135                StartupError::InvalidConfig("max_active_transactions must be nonzero")
136            }
137            ReservationPoolError::ZeroChannel => {
138                StartupError::InvalidConfig("channel capacity must be nonzero")
139            }
140        })?;
141
142        // Start queue: exactly max_active (spec §9.2), unless a test overrides
143        // capacity to prove start-full rollback with reservation headroom (D-040).
144        // Control queue is separate so cancel/shutdown cannot be starved; capacity
145        // comes from `TransactionLimits.max_actor_commands` (not max_active+8).
146        let start_capacity = bootstrap.config.start_queue_capacity.unwrap_or(max_active);
147        let (start_tx, start_rx) = mpsc::channel(start_capacity);
148        let control_capacity = bootstrap
149            .config
150            .transaction_limits
151            .max_actor_commands
152            .max(1);
153        let (control_tx, control_rx) = mpsc::channel(control_capacity);
154        let worker_capacity = max_active.saturating_add(8);
155        let (worker_tx, worker_rx) = mpsc::channel::<WorkerMessage>(worker_capacity);
156        let spawn_capacity = max_active.saturating_mul(8).max(32);
157        let (task_spawner, spawn_rx) =
158            super::task_spawner::TransactionTaskSpawner::channel(spawn_capacity);
159        let channels = Arc::new(live);
160        let shared = Arc::new(RuntimeShared {
161            state: AtomicU8::new(STATE_STARTING),
162            ledger: Mutex::new(LifecycleLedger::new()),
163            start_tx,
164            control_tx,
165            worker_tx,
166            wake: Notify::new(),
167            channels: Arc::clone(&channels),
168            default_deadline: bootstrap.config.transaction_limits.transaction_deadline,
169            cleanup_deadline: bootstrap.config.transaction_limits.cleanup_deadline,
170            terminal_event_delivery_deadline: bootstrap
171                .config
172                .transaction_limits
173                .terminal_event_delivery_deadline,
174            task_spawner,
175            shutdown_generation: AtomicU64::new(0),
176            shutdown_report: Mutex::new(None),
177            completions_published: AtomicU64::new(0),
178            completions_receiver_dropped: AtomicU64::new(0),
179            completions_invariant_failed: AtomicU64::new(0),
180            runtime_shutdown_terminals: AtomicU64::new(0),
181            owned_tasks: AtomicU32::new(0),
182            live_connector_owners: AtomicU32::new(0),
183            enable_mcp_listener: bootstrap.config.enable_mcp_listener,
184            mcp_listen_addr: Mutex::new(None),
185            mcp_gateway: Mutex::new(None),
186            mcp_cancel: Mutex::new(None),
187            block_stopped: bootstrap.config.block_stopped.clone(),
188            hold_start: bootstrap.config.hold_start.clone(),
189            hold_control: bootstrap.config.hold_control.clone(),
190            hold_finalizer_after_seal: bootstrap.config.hold_finalizer_after_seal.clone(),
191            hold_executor_teardown: bootstrap.config.hold_executor_teardown.clone(),
192            inject_non_yielding_service: bootstrap.config.inject_non_yielding_service,
193            inject_join_only_spill: bootstrap.config.inject_join_only_spill.clone(),
194            drain_complete: std::sync::atomic::AtomicBool::new(false),
195            tools_registry: bootstrap.tools.clone(),
196            shared_tool_capacity: crate::transaction::tool_capacity::SharedToolCapacity::new(
197                bootstrap
198                    .config
199                    .transaction_limits
200                    .max_active_transactions
201                    .saturating_mul(4)
202                    .max(8),
203            ),
204            tool_spill: Arc::new(crate::transaction::dispatcher::OrphanToolPermitSet::new()),
205            owned_processes: Arc::new(AtomicU32::new(0)),
206            process_registry: Arc::new(
207                crate::transaction::owned_process_registry::OwnedProcessRegistry::new(),
208            ),
209            transaction_limits: bootstrap.config.transaction_limits.clone(),
210        });
211
212        let (ready_tx, ready_rx) = std::sync::mpsc::channel::<Result<(), StartupError>>();
213        let (exited_tx, exited_rx) = oneshot::channel::<()>();
214        let teardown_gate = bootstrap.config.hold_executor_teardown.clone();
215        let shared_thread = Arc::clone(&shared);
216        let mcp_max_routes = bootstrap
217            .config
218            .transaction_limits
219            .max_active_transactions
220            .saturating_mul(2)
221            .max(DEFAULT_MCP_MAX_ROUTES);
222        let thread = std::thread::Builder::new()
223            .name("monoloop-runtime".into())
224            .spawn(move || {
225                // Two workers is enough for coordinator+publisher; keep the
226                // pool small so parallel integration tests do not oversubscribe.
227                let rt = match tokio::runtime::Builder::new_multi_thread()
228                    .worker_threads(2)
229                    .max_blocking_threads(2)
230                    .enable_all()
231                    .thread_name("monoloop-worker")
232                    .build()
233                {
234                    Ok(rt) => rt,
235                    Err(_) => {
236                        let _ = ready_tx.send(Err(StartupError::ExecutorUnavailable));
237                        return;
238                    }
239                };
240                // Bind + prepare + publish MCP inside the executor before
241                // Accepting so enable_mcp_listener fails closed and §7.1
242                // `start` returns only after the gateway handle is ready.
243                // Serve is TaskSupervisor-owned (no ambient spawn).
244                rt.block_on(async {
245                    let mcp_prepared = if shared_thread.enable_mcp_listener {
246                        match tokio::net::TcpListener::bind("127.0.0.1:0").await {
247                            Ok(listener) => {
248                                let request_owner: Option<
249                                    std::sync::Arc<dyn crate::transaction::mcp::McpRequestOwner>,
250                                > = Some(std::sync::Arc::new(
251                                    super::mcp_request_owner::SupervisedMcpRequestOwner::new(
252                                        shared_thread.task_spawner.clone(),
253                                    ),
254                                ));
255                                match McpGateway::prepare_from_tokio_listener(
256                                    listener,
257                                    mcp_max_routes,
258                                    request_owner,
259                                ) {
260                                    Ok(prepared) => {
261                                        super::mcp_listener::publish_runtime_mcp(
262                                            &shared_thread,
263                                            &prepared,
264                                        );
265                                        Some(prepared)
266                                    }
267                                    Err(_) => {
268                                        let _ = ready_tx.send(Err(StartupError::InvalidConfig(
269                                            "MCP gateway prepare failed",
270                                        )));
271                                        return;
272                                    }
273                                }
274                            }
275                            Err(_) => {
276                                let _ = ready_tx.send(Err(StartupError::InvalidConfig(
277                                    "MCP loopback bind failed",
278                                )));
279                                return;
280                            }
281                        }
282                    } else {
283                        None
284                    };
285                    shared_thread.state.store(STATE_ACCEPTING, Ordering::SeqCst);
286                    let _ = ready_tx.send(Ok(()));
287                    run_supervisor(
288                        shared_thread,
289                        start_rx,
290                        control_rx,
291                        worker_rx,
292                        spawn_rx,
293                        mcp_prepared,
294                    )
295                    .await;
296                });
297                // D-049: optional test gate after drain, before executor teardown.
298                if let Some(gate) = teardown_gate {
299                    let rt_gate = tokio::runtime::Builder::new_current_thread()
300                        .enable_all()
301                        .build();
302                    if let Ok(rt_gate) = rt_gate {
303                        rt_gate.block_on(gate.wait_released());
304                    }
305                }
306                // Bounded executor teardown so Drop/join cannot strand on
307                // residual background work after the supervisor has returned.
308                rt.shutdown_timeout(Duration::from_secs(2));
309                let _ = exited_tx.send(());
310            })
311            .map_err(|_| StartupError::ExecutorUnavailable)?;
312
313        ready_rx
314            .recv_timeout(Duration::from_secs(5))
315            .map_err(|_| StartupError::ExecutorUnavailable)??;
316
317        let tools = bootstrap.tools;
318        let max_tools = bootstrap
319            .config
320            .transaction_limits
321            .max_tools_per_transaction;
322        let handle = TransactionRuntimeHandle {
323            shared: Arc::clone(&shared),
324            pool: Arc::clone(&pool),
325            max_tools,
326            channels: Arc::clone(&channels),
327            tools,
328        };
329        let owner = RuntimeOwner {
330            shared,
331            thread: Some(thread),
332            thread_exited: Some(exited_rx),
333            pool,
334            channels,
335        };
336        Ok(StartedRuntime { owner, handle })
337    }
338}
339
340impl RuntimeOwner {
341    /// Current lifecycle state.
342    pub fn state(&self) -> RuntimeState {
343        self.shared.runtime_state()
344    }
345
346    /// Active ledger entry count.
347    pub fn ledger_len(&self) -> usize {
348        self.shared.ledger.lock().map(|l| l.len()).unwrap_or(0)
349    }
350
351    /// Supervisor-owned task count (§22.3 stopped proof).
352    pub fn owned_task_count(&self) -> u32 {
353        self.shared.owned_tasks.load(Ordering::SeqCst)
354    }
355
356    /// Live ConnectorOwner tasks (register-before-I/O; Hang-ready observation).
357    pub fn live_connector_owners(&self) -> u32 {
358        self.shared.live_connector_owners.load(Ordering::SeqCst)
359    }
360
361    /// Runtime-scoped tool spill pending count (joins + orphans; §22.4 / Stopped gate).
362    pub fn tool_spill_pending(&self) -> usize {
363        self.shared.tool_spill.pending_count()
364    }
365
366    /// Global reservation count.
367    pub fn global_reservations(&self) -> usize {
368        self.pool.global_active()
369    }
370
371    /// Channel reservation count (D-040 rollback observability).
372    pub fn channel_reservations(&self, channel: &ChannelId) -> usize {
373        self.pool.channel_active(channel)
374    }
375
376    /// Number of realized channels owned by this runtime.
377    pub fn channel_count(&self) -> usize {
378        self.channels.len()
379    }
380
381    /// Bound MCP loopback address when `enable_mcp_listener` and the gateway is live.
382    pub fn mcp_local_addr(&self) -> Option<std::net::SocketAddr> {
383        self.shared.mcp_listen_addr.lock().ok().and_then(|g| *g)
384    }
385
386    /// Cloneable MCP gateway handle while the RuntimeService is live.
387    pub fn mcp_gateway(&self) -> Option<McpGatewayHandle> {
388        self.shared.mcp_gateway.lock().ok().and_then(|g| g.clone())
389    }
390
391    /// Begin shutdown (idempotent). Synchronously moves admission to Quiescing.
392    ///
393    /// Control delivery is best-effort on the control queue; the supervisor also
394    /// observes `Quiescing` via an internal wake so a full control queue cannot
395    /// strand the runtime.
396    pub fn begin_shutdown(&self) -> ShutdownTicket {
397        // §18.2 / D-010: Quiescing transition under the same lock admit uses for
398        // install, so a concurrent admit either inserts before the flip (and is
399        // visible to the shutdown snapshot) or sees Quiescing and rejects.
400        {
401            let _ledger = self.shared.ledger.lock().unwrap_or_else(|e| e.into_inner());
402            let _ = self.shared.state.compare_exchange(
403                STATE_ACCEPTING,
404                STATE_QUIESCING,
405                Ordering::SeqCst,
406                Ordering::SeqCst,
407            );
408            if self.shared.state.load(Ordering::SeqCst) == STATE_STARTING {
409                self.shared.state.store(STATE_QUIESCING, Ordering::SeqCst);
410            }
411        }
412        // CAS 0→1 elects a single announcer; losers observe the published
413        // generation immediately — no spin/yield race (§22.5).
414        let generation = match self.shared.shutdown_generation.compare_exchange(
415            0,
416            1,
417            Ordering::SeqCst,
418            Ordering::SeqCst,
419        ) {
420            Ok(_) => 1,
421            Err(existing) => existing,
422        };
423        let _ = self
424            .shared
425            .control_tx
426            .try_send(ControlCommand::BeginShutdown);
427        self.shared.wake.notify_waiters();
428        ShutdownTicket { generation }
429    }
430
431    /// Wait until Stopped or the deadline elapses (v2: timeout ⇒ Quiescing, not false Stopped).
432    ///
433    /// D-049: the deadline bounds the **entire** API — including the executor OS
434    /// thread join. Public `Stopped` is published only after that join.
435    pub async fn wait_stopped(&mut self, deadline: Duration) -> ShutdownWaitOutcome {
436        if self.shared.state.load(Ordering::SeqCst) == STATE_ACCEPTING {
437            let _ = self.begin_shutdown();
438        }
439        if self.shared.state.load(Ordering::SeqCst) == STATE_STOPPED && self.thread.is_none() {
440            let report = self
441                .shared
442                .shutdown_report
443                .lock()
444                .unwrap_or_else(|e| e.into_inner())
445                .clone()
446                .unwrap_or_else(|| self.shared.final_report());
447            return ShutdownWaitOutcome::Stopped(report);
448        }
449
450        let start = tokio::time::Instant::now();
451        // Phase 1: supervisor drain (state stays Quiescing until join).
452        if let Err(timed_out) = wait_until_drain_complete(&self.shared, deadline).await {
453            return timed_out;
454        }
455
456        let _ = self
457            .shared
458            .control_tx
459            .try_send(ControlCommand::StopSupervisor);
460        self.shared.wake.notify_one();
461
462        // Phase 2: wait for executor thread exit within remaining budget.
463        let remaining = deadline.saturating_sub(start.elapsed());
464        if let Some(rx) = self.thread_exited.as_mut() {
465            match tokio::time::timeout(remaining, &mut *rx).await {
466                Ok(Ok(())) | Ok(Err(_)) => {
467                    self.thread_exited = None;
468                }
469                Err(_) => {
470                    // Retain join handle + exited receiver for a later wait.
471                    return ShutdownWaitOutcome::TimedOut(self.shared.snapshot());
472                }
473            }
474        }
475        if let Some(thread) = self.thread.take() {
476            // Exited signal observed — join should return promptly.
477            let _ = thread.join();
478        }
479        self.shared.state.store(STATE_STOPPED, Ordering::SeqCst);
480        let report = self
481            .shared
482            .shutdown_report
483            .lock()
484            .unwrap_or_else(|e| e.into_inner())
485            .clone()
486            .unwrap_or_else(|| self.shared.final_report());
487        ShutdownWaitOutcome::Stopped(report)
488    }
489}
490
491impl Drop for RuntimeOwner {
492    fn drop(&mut self) {
493        // Never strand Drop behind test-only hold gates.
494        if let Some(gate) = self.shared.block_stopped.as_ref() {
495            gate.release();
496        }
497        if let Some(gate) = self.shared.hold_start.as_ref() {
498            gate.release();
499        }
500        if let Some(gate) = self.shared.hold_control.as_ref() {
501            gate.release();
502        }
503        if let Some(gate) = self.shared.hold_finalizer_after_seal.as_ref() {
504            gate.release();
505        }
506        if let Some(gate) = self.shared.hold_executor_teardown.as_ref() {
507            gate.release();
508        }
509        if let Some(inject) = self.shared.inject_join_only_spill.as_ref() {
510            inject.release();
511        }
512        if self.shared.state.load(Ordering::SeqCst) != STATE_STOPPED {
513            let _ = self.begin_shutdown();
514            let _ = self
515                .shared
516                .control_tx
517                .try_send(ControlCommand::StopSupervisor);
518            self.shared.wake.notify_one();
519        }
520        // §18.4: Drop MUST preserve ownership — join the executor OS thread.
521        // MAY block indefinitely on non-cooperative in-process work. MUST NOT
522        // detach, abandon a live join handle, or invent a successful stop.
523        // Hosts that need bounded process-exit MUST use ProcessIsolated for
524        // untrusted work and complete explicit shutdown before dropping.
525        if let Some(thread) = self.thread.take() {
526            // Observe the join either way — panic on the executor thread is still
527            // ownership-complete (§18.4). Publish Stopped only after join (D-049).
528            match thread.join() {
529                Ok(()) => {
530                    self.shared.state.store(STATE_STOPPED, Ordering::SeqCst);
531                }
532                Err(_) => {
533                    self.shared.state.store(STATE_STOPPED, Ordering::SeqCst);
534                }
535            }
536        }
537        self.thread_exited = None;
538    }
539}
540
541impl TransactionRuntimeHandle {
542    /// Current lifecycle state.
543    pub fn state(&self) -> RuntimeState {
544        self.shared.runtime_state()
545    }
546
547    /// Bound MCP loopback address when the gateway RuntimeService is live.
548    pub fn mcp_local_addr(&self) -> Option<std::net::SocketAddr> {
549        self.shared.mcp_listen_addr.lock().ok().and_then(|g| *g)
550    }
551
552    /// Cloneable MCP gateway handle while the RuntimeService is live.
553    pub fn mcp_gateway(&self) -> Option<McpGatewayHandle> {
554        self.shared.mcp_gateway.lock().ok().and_then(|g| g.clone())
555    }
556
557    /// Synchronously admit a v2 transaction (no spawn / no executor wait).
558    pub fn submit(
559        &self,
560        request: TransactionSubmitRequest,
561    ) -> Result<AdmissionReceipt, AdmissionError> {
562        admit(
563            &self.shared,
564            &self.pool,
565            self.channels.as_ref(),
566            &self.tools,
567            self.max_tools,
568            request,
569        )
570    }
571
572    /// Request cancellation or forced termination.
573    pub fn terminate(
574        &self,
575        selector: TransactionSelector,
576        mode: TerminationMode,
577    ) -> TerminationDisposition {
578        let tx = match selector {
579            TransactionSelector::Transaction(id) => id,
580            TransactionSelector::Session(key) => {
581                let ledger = self.shared.ledger.lock().unwrap_or_else(|e| e.into_inner());
582                match ledger.transaction_for_session(&key) {
583                    Some(id) => id,
584                    None => return TerminationDisposition::NotFound,
585                }
586            }
587        };
588        // Honest ledger check before enqueue (D-039): never lie Full→AlreadyTerminal.
589        // §22.2: Cancelled may still be upgraded to ForceTerminate.
590        {
591            let ledger = self.shared.ledger.lock().unwrap_or_else(|e| e.into_inner());
592            match ledger.get(&tx) {
593                None => return TerminationDisposition::NotFound,
594                Some(entry) => {
595                    if let Some(term) = entry.terminal.as_ref() {
596                        let upgrade = matches!(mode, TerminationMode::ForceTerminate { .. })
597                            && term.kind == monoloop_contracts::TransactionEndKind::Cancelled;
598                        if !upgrade {
599                            return TerminationDisposition::AlreadyTerminal;
600                        }
601                    }
602                }
603            }
604        }
605        let cmd = match mode {
606            TerminationMode::Cancel { .. } => ControlCommand::Cancel(tx),
607            TerminationMode::ForceTerminate { .. } => ControlCommand::ForceTerminate(tx),
608        };
609        match self.shared.control_tx.try_send(cmd) {
610            Ok(()) => {
611                self.shared.wake.notify_one();
612                TerminationDisposition::Accepted
613            }
614            Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
615                TerminationDisposition::ControlCapacityExceeded
616            }
617            Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
618                TerminationDisposition::RuntimeClosed
619            }
620        }
621    }
622}
623
624fn clone_binding(binding: &ChannelBinding) -> ChannelBinding {
625    ChannelBinding {
626        id: binding.id.clone(),
627        kind: binding.kind,
628        tool_mode: binding.tool_mode,
629        connector_factory: Arc::clone(&binding.connector_factory),
630        encoder: Arc::clone(&binding.encoder),
631        interpreter: Arc::clone(&binding.interpreter),
632        endpoint_ref: binding.endpoint_ref.clone(),
633        credential_ref: binding.credential_ref.clone(),
634        defaults: binding.defaults.clone(),
635        capabilities: binding.capabilities.clone(),
636        limits: binding.limits.clone(),
637    }
638}