Skip to main content

agentos_runtime/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Process-owned trusted runtime services.
4//!
5//! Production sidecars construct exactly one [`SidecarRuntime`] at their
6//! process entrypoint. Subsystems receive a clone of [`RuntimeContext`]; they
7//! never construct Tokio runtimes of their own.
8
9use std::cell::Cell;
10use std::fmt;
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
14use std::sync::{mpsc, Arc, Mutex, OnceLock};
15use std::task::{Context, Poll};
16use std::thread;
17use std::time::{Duration, Instant};
18
19use accounting::{LimitError, Reservation, ResourceClass, ResourceLedger, ResourceLimit};
20use fairness::{FairBudget, FairWorkBroker, FairnessConfig};
21use metrics::{
22    ExecutorMetricClass, RuntimeMetrics, TelemetryFallback, TelemetryFallbackCode,
23    TelemetrySeverity, TelemetrySubsystem, WatchdogMetric,
24};
25
26pub mod accounting;
27pub mod capability;
28pub mod fairness;
29pub mod metrics;
30pub mod readiness;
31pub mod supervision;
32
33pub use supervision::{
34    TaskClass, TaskClassSnapshot, TaskOwner, TaskSpawnError, TaskSupervisor, TaskTerminalReason,
35    TaskTerminalReport,
36};
37
38const DEFAULT_MAX_BLOCKING_JOB_BYTES: usize = 64 * 1024 * 1024;
39const DEFAULT_BLOCKING_JOB_TIMEOUT_MS: u64 = 30_000;
40const DEFAULT_MAX_BLOCKING_JOBS: usize = 1_028;
41const DEFAULT_MAX_QUEUED_BLOCKING_JOBS: usize = 1024;
42const DEFAULT_MAX_PROCESS_CAPABILITIES: usize = 16_384;
43const DEFAULT_MAX_PROCESS_SOCKETS: usize = 8_192;
44const DEFAULT_MAX_PROCESS_CONNECTIONS: usize = 8_192;
45const DEFAULT_MAX_PROCESS_SOCKET_BUFFERED_BYTES: usize = 1024 * 1024 * 1024;
46const DEFAULT_MAX_PROCESS_DATAGRAMS: usize = 65_536;
47const DEFAULT_MAX_PROCESS_TIMERS: usize = 65_536;
48const DEFAULT_MAX_PROCESS_TASKS: usize = 65_536;
49const DEFAULT_MAX_PROCESS_READY_HANDLES: usize = 16_384;
50const DEFAULT_MAX_PROCESS_HANDLE_COMMANDS: usize = 65_536;
51const DEFAULT_MAX_PROCESS_HANDLE_COMMAND_BYTES: usize = 256 * 1024 * 1024;
52const DEFAULT_MAX_PROCESS_BRIDGE_CALLS: usize = 65_536;
53const DEFAULT_MAX_PROCESS_BRIDGE_REQUEST_BYTES: usize = 256 * 1024 * 1024;
54const DEFAULT_MAX_PROCESS_BRIDGE_RESPONSE_BYTES: usize = 256 * 1024 * 1024;
55const DEFAULT_MAX_PROCESS_ASYNC_COMPLETIONS: usize = 65_536;
56const DEFAULT_MAX_PROCESS_ASYNC_COMPLETION_BYTES: usize = 256 * 1024 * 1024;
57const DEFAULT_MAX_PROCESS_UDP_DATAGRAMS: usize = 65_536;
58const DEFAULT_MAX_PROCESS_UDP_BYTES: usize = 256 * 1024 * 1024;
59const DEFAULT_MAX_PROCESS_TLS_BYTES: usize = 256 * 1024 * 1024;
60const DEFAULT_MAX_PROCESS_HTTP2_CONNECTIONS: usize = 4_096;
61const DEFAULT_MAX_PROCESS_HTTP2_STREAMS: usize = 65_536;
62const DEFAULT_MAX_PROCESS_HTTP2_BYTES: usize = 512 * 1024 * 1024;
63const DEFAULT_MAX_PROCESS_HTTP2_HEADER_BYTES: usize = 128 * 1024 * 1024;
64const DEFAULT_MAX_PROCESS_HTTP2_DATA_BYTES: usize = 512 * 1024 * 1024;
65const DEFAULT_MAX_PROCESS_HTTP2_COMMANDS: usize = 65_536;
66const DEFAULT_MAX_PROCESS_HTTP2_COMMAND_BYTES: usize = 256 * 1024 * 1024;
67const DEFAULT_MAX_PROCESS_HTTP2_EVENTS: usize = 65_536;
68const DEFAULT_MAX_PROCESS_HTTP2_EVENT_BYTES: usize = 512 * 1024 * 1024;
69const DEFAULT_TASK_POLL_WATCHDOG_MS: u64 = 100;
70const DEFAULT_MAX_TERMINAL_TASK_REPORTS: usize = 4_096;
71const DEFAULT_VM_EXECUTOR_TEARDOWN_TIMEOUT_MS: u64 = 5_000;
72pub const DEFAULT_PROTOCOL_MAX_INGRESS_FRAMES: usize = 128;
73pub const DEFAULT_PROTOCOL_MAX_INGRESS_BYTES: usize = 64 * 1024 * 1024;
74pub const DEFAULT_PROTOCOL_MAX_SESSIONS_PER_CONNECTION: usize = 4_096;
75pub const DEFAULT_PROTOCOL_MAX_CONTROL_FRAMES: usize = 1_024;
76pub const DEFAULT_PROTOCOL_MAX_CONTROL_BYTES: usize = 64 * 1024 * 1024;
77pub const DEFAULT_PROTOCOL_MAX_EGRESS_FRAMES: usize = 4_096;
78pub const DEFAULT_PROTOCOL_MAX_EGRESS_BYTES: usize = 256 * 1024 * 1024;
79pub const DEFAULT_PROTOCOL_MAX_IN_FLIGHT_REQUESTS: usize = 128;
80pub const DEFAULT_PROTOCOL_MAX_IN_FLIGHT_REQUEST_BYTES: usize = 64 * 1024 * 1024;
81pub const DEFAULT_PROTOCOL_MAX_TERMINAL_FRAMES: usize = 128;
82pub const DEFAULT_PROTOCOL_MAX_TERMINAL_BYTES: usize = 8 * 1024 * 1024;
83pub const DEFAULT_PROTOCOL_TERMINAL_FALLBACK_BYTES: usize = 16 * 1024;
84pub const DEFAULT_PROTOCOL_MAX_PROGRESS_FRAMES: usize = 256;
85pub const DEFAULT_PROTOCOL_MAX_PROGRESS_BYTES: usize = 32 * 1024 * 1024;
86pub const DEFAULT_PROTOCOL_MAX_REJECTION_FRAMES: usize = 128;
87pub const DEFAULT_PROTOCOL_MAX_REJECTION_BYTES: usize = 4 * 1024 * 1024;
88pub const DEFAULT_PROTOCOL_SHUTDOWN_GRACE_TIMEOUT_MS: u64 = 5_000;
89pub const DEFAULT_PROTOCOL_MAX_PENDING_RESPONSES: usize = 10_000;
90pub const DEFAULT_PROTOCOL_MAX_PENDING_RESPONSE_BYTES: usize = 256 * 1024 * 1024;
91pub const DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS: usize = 10_000;
92pub const DEFAULT_PROTOCOL_MAX_OUTBOUND_REQUESTS: usize = 10_000;
93pub const DEFAULT_PROTOCOL_MAX_COMPLETED_RESPONSES: usize = 10_000;
94const DEFAULT_FAIRNESS_VM_OPERATIONS: usize = 64;
95const DEFAULT_FAIRNESS_VM_BYTES: usize = 1024 * 1024;
96const DEFAULT_FAIRNESS_CAPABILITY_OPERATIONS: usize = 16;
97const DEFAULT_FAIRNESS_CAPABILITY_BYTES: usize = 256 * 1024;
98const DEFAULT_FAIRNESS_MAX_VMS: usize = 4_096;
99const DEFAULT_FAIRNESS_MAX_CAPABILITIES_PER_VM: usize = 16_384;
100
101thread_local! {
102    static IS_AGENTOS_RUNTIME_WORKER: Cell<bool> = const { Cell::new(false) };
103}
104
105/// Whether the current OS thread is one of the process runtime's fixed workers.
106///
107/// Synchronous compatibility adapters use this to reject waits that would
108/// consume a trusted runtime worker and could deadlock the work they submitted.
109pub fn is_runtime_worker_thread() -> bool {
110    IS_AGENTOS_RUNTIME_WORKER.with(Cell::get)
111}
112
113/// Process-owned bounds for the multiplexed sidecar transport.
114///
115/// Ordinary frames and response/control frames have independent admission so
116/// an event or request backlog cannot consume the capacity needed to settle an
117/// already-registered bridge call. Byte ceilings cover retained decoded or
118/// encoded frames; the one active decoder is separately bounded by the wire
119/// codec's `max_frame_bytes` setting.
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct RuntimeProtocolConfig {
122    pub max_ingress_frames: usize,
123    pub max_ingress_bytes: usize,
124    /// Live session membership retained for one authenticated connection.
125    pub max_sessions_per_connection: usize,
126    pub max_control_frames: usize,
127    pub max_control_bytes: usize,
128    pub max_egress_frames: usize,
129    pub max_egress_bytes: usize,
130    /// Requests admitted for independent execution. The ingress channel is the
131    /// only pre-admission backlog; there is no ordinary request FIFO after it.
132    pub max_in_flight_requests: usize,
133    pub max_in_flight_request_bytes: usize,
134    /// Capacity reserved for exactly one terminal response per admitted
135    /// request. `terminal_fallback_bytes` is charged at request admission.
136    pub max_terminal_frames: usize,
137    pub max_terminal_bytes: usize,
138    pub terminal_fallback_bytes: usize,
139    /// Physically control-lane output that must be able to unblock active work.
140    pub max_progress_frames: usize,
141    pub max_progress_bytes: usize,
142    /// Typed responses for requests rejected before ordinary admission.
143    pub max_rejection_frames: usize,
144    pub max_rejection_bytes: usize,
145    /// Finite deadline for draining already-admitted work after ordinary
146    /// request admission has closed during protocol shutdown.
147    pub shutdown_grace_ms: u64,
148    pub max_pending_responses: usize,
149    pub max_pending_response_bytes: usize,
150    pub max_process_events: usize,
151    pub max_outbound_requests: usize,
152    pub max_completed_responses: usize,
153}
154
155impl Default for RuntimeProtocolConfig {
156    fn default() -> Self {
157        Self {
158            max_ingress_frames: DEFAULT_PROTOCOL_MAX_INGRESS_FRAMES,
159            max_ingress_bytes: DEFAULT_PROTOCOL_MAX_INGRESS_BYTES,
160            max_sessions_per_connection: DEFAULT_PROTOCOL_MAX_SESSIONS_PER_CONNECTION,
161            max_control_frames: DEFAULT_PROTOCOL_MAX_CONTROL_FRAMES,
162            max_control_bytes: DEFAULT_PROTOCOL_MAX_CONTROL_BYTES,
163            max_egress_frames: DEFAULT_PROTOCOL_MAX_EGRESS_FRAMES,
164            max_egress_bytes: DEFAULT_PROTOCOL_MAX_EGRESS_BYTES,
165            max_in_flight_requests: DEFAULT_PROTOCOL_MAX_IN_FLIGHT_REQUESTS,
166            max_in_flight_request_bytes: DEFAULT_PROTOCOL_MAX_IN_FLIGHT_REQUEST_BYTES,
167            max_terminal_frames: DEFAULT_PROTOCOL_MAX_TERMINAL_FRAMES,
168            max_terminal_bytes: DEFAULT_PROTOCOL_MAX_TERMINAL_BYTES,
169            terminal_fallback_bytes: DEFAULT_PROTOCOL_TERMINAL_FALLBACK_BYTES,
170            max_progress_frames: DEFAULT_PROTOCOL_MAX_PROGRESS_FRAMES,
171            max_progress_bytes: DEFAULT_PROTOCOL_MAX_PROGRESS_BYTES,
172            max_rejection_frames: DEFAULT_PROTOCOL_MAX_REJECTION_FRAMES,
173            max_rejection_bytes: DEFAULT_PROTOCOL_MAX_REJECTION_BYTES,
174            shutdown_grace_ms: DEFAULT_PROTOCOL_SHUTDOWN_GRACE_TIMEOUT_MS,
175            max_pending_responses: DEFAULT_PROTOCOL_MAX_PENDING_RESPONSES,
176            max_pending_response_bytes: DEFAULT_PROTOCOL_MAX_PENDING_RESPONSE_BYTES,
177            max_process_events: DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS,
178            max_outbound_requests: DEFAULT_PROTOCOL_MAX_OUTBOUND_REQUESTS,
179            max_completed_responses: DEFAULT_PROTOCOL_MAX_COMPLETED_RESPONSES,
180        }
181    }
182}
183
184#[derive(Clone, Debug, PartialEq, Eq)]
185pub struct RuntimeFairnessConfig {
186    pub vm_quantum_operations: usize,
187    pub vm_quantum_bytes: usize,
188    pub capability_quantum_operations: usize,
189    pub capability_quantum_bytes: usize,
190    pub max_vm_deficit_operations: usize,
191    pub max_vm_deficit_bytes: usize,
192    pub max_capability_deficit_operations: usize,
193    pub max_capability_deficit_bytes: usize,
194    pub max_vms: usize,
195    pub max_capabilities_per_vm: usize,
196}
197
198impl Default for RuntimeFairnessConfig {
199    fn default() -> Self {
200        Self {
201            vm_quantum_operations: DEFAULT_FAIRNESS_VM_OPERATIONS,
202            vm_quantum_bytes: DEFAULT_FAIRNESS_VM_BYTES,
203            capability_quantum_operations: DEFAULT_FAIRNESS_CAPABILITY_OPERATIONS,
204            capability_quantum_bytes: DEFAULT_FAIRNESS_CAPABILITY_BYTES,
205            max_vm_deficit_operations: DEFAULT_FAIRNESS_VM_OPERATIONS * 4,
206            max_vm_deficit_bytes: DEFAULT_FAIRNESS_VM_BYTES * 4,
207            max_capability_deficit_operations: DEFAULT_FAIRNESS_CAPABILITY_OPERATIONS * 4,
208            max_capability_deficit_bytes: DEFAULT_FAIRNESS_CAPABILITY_BYTES * 4,
209            max_vms: DEFAULT_FAIRNESS_MAX_VMS,
210            max_capabilities_per_vm: DEFAULT_FAIRNESS_MAX_CAPABILITIES_PER_VM,
211        }
212    }
213}
214
215impl RuntimeFairnessConfig {
216    fn scheduler_config(&self) -> FairnessConfig {
217        FairnessConfig {
218            vm_quantum: FairBudget::new(self.vm_quantum_operations, self.vm_quantum_bytes),
219            capability_quantum: FairBudget::new(
220                self.capability_quantum_operations,
221                self.capability_quantum_bytes,
222            ),
223            max_vm_deficit: FairBudget::new(
224                self.max_vm_deficit_operations,
225                self.max_vm_deficit_bytes,
226            ),
227            max_capability_deficit: FairBudget::new(
228                self.max_capability_deficit_operations,
229                self.max_capability_deficit_bytes,
230            ),
231            max_vms: self.max_vms,
232            max_capabilities_per_vm: self.max_capabilities_per_vm,
233        }
234    }
235}
236
237#[derive(Clone, Debug, PartialEq, Eq)]
238pub struct RuntimeResourceConfig {
239    pub max_capabilities: usize,
240    pub max_ready_handles: usize,
241    pub max_sockets: usize,
242    pub max_connections: usize,
243    pub max_socket_buffered_bytes: usize,
244    pub max_datagrams: usize,
245    pub max_timers: usize,
246    pub max_tasks: usize,
247    pub max_handle_commands: usize,
248    pub max_handle_command_bytes: usize,
249    pub max_bridge_calls: usize,
250    pub max_bridge_request_bytes: usize,
251    pub max_bridge_response_bytes: usize,
252    pub max_async_completions: usize,
253    pub max_async_completion_bytes: usize,
254    pub max_udp_datagrams: usize,
255    pub max_udp_bytes: usize,
256    pub max_tls_bytes: usize,
257    pub max_http2_connections: usize,
258    pub max_http2_streams: usize,
259    pub max_http2_buffered_bytes: usize,
260    pub max_http2_header_bytes: usize,
261    pub max_http2_data_bytes: usize,
262    pub max_http2_commands: usize,
263    pub max_http2_command_bytes: usize,
264    pub max_http2_events: usize,
265    pub max_http2_event_bytes: usize,
266}
267
268impl Default for RuntimeResourceConfig {
269    fn default() -> Self {
270        Self {
271            max_capabilities: DEFAULT_MAX_PROCESS_CAPABILITIES,
272            max_ready_handles: DEFAULT_MAX_PROCESS_READY_HANDLES,
273            max_sockets: DEFAULT_MAX_PROCESS_SOCKETS,
274            max_connections: DEFAULT_MAX_PROCESS_CONNECTIONS,
275            max_socket_buffered_bytes: DEFAULT_MAX_PROCESS_SOCKET_BUFFERED_BYTES,
276            max_datagrams: DEFAULT_MAX_PROCESS_DATAGRAMS,
277            max_timers: DEFAULT_MAX_PROCESS_TIMERS,
278            max_tasks: DEFAULT_MAX_PROCESS_TASKS,
279            max_handle_commands: DEFAULT_MAX_PROCESS_HANDLE_COMMANDS,
280            max_handle_command_bytes: DEFAULT_MAX_PROCESS_HANDLE_COMMAND_BYTES,
281            max_bridge_calls: DEFAULT_MAX_PROCESS_BRIDGE_CALLS,
282            max_bridge_request_bytes: DEFAULT_MAX_PROCESS_BRIDGE_REQUEST_BYTES,
283            max_bridge_response_bytes: DEFAULT_MAX_PROCESS_BRIDGE_RESPONSE_BYTES,
284            max_async_completions: DEFAULT_MAX_PROCESS_ASYNC_COMPLETIONS,
285            max_async_completion_bytes: DEFAULT_MAX_PROCESS_ASYNC_COMPLETION_BYTES,
286            max_udp_datagrams: DEFAULT_MAX_PROCESS_UDP_DATAGRAMS,
287            max_udp_bytes: DEFAULT_MAX_PROCESS_UDP_BYTES,
288            max_tls_bytes: DEFAULT_MAX_PROCESS_TLS_BYTES,
289            max_http2_connections: DEFAULT_MAX_PROCESS_HTTP2_CONNECTIONS,
290            max_http2_streams: DEFAULT_MAX_PROCESS_HTTP2_STREAMS,
291            max_http2_buffered_bytes: DEFAULT_MAX_PROCESS_HTTP2_BYTES,
292            max_http2_header_bytes: DEFAULT_MAX_PROCESS_HTTP2_HEADER_BYTES,
293            max_http2_data_bytes: DEFAULT_MAX_PROCESS_HTTP2_DATA_BYTES,
294            max_http2_commands: DEFAULT_MAX_PROCESS_HTTP2_COMMANDS,
295            max_http2_command_bytes: DEFAULT_MAX_PROCESS_HTTP2_COMMAND_BYTES,
296            max_http2_events: DEFAULT_MAX_PROCESS_HTTP2_EVENTS,
297            max_http2_event_bytes: DEFAULT_MAX_PROCESS_HTTP2_EVENT_BYTES,
298        }
299    }
300}
301
302impl RuntimeResourceConfig {
303    fn limits(&self) -> Vec<(ResourceClass, ResourceLimit)> {
304        vec![
305            (
306                ResourceClass::Capabilities,
307                ResourceLimit::new(self.max_capabilities, "runtime.resources.maxCapabilities"),
308            ),
309            (
310                ResourceClass::ReadyHandles,
311                ResourceLimit::new(self.max_ready_handles, "runtime.resources.maxReadyHandles"),
312            ),
313            (
314                ResourceClass::Sockets,
315                ResourceLimit::new(self.max_sockets, "runtime.resources.maxSockets"),
316            ),
317            (
318                ResourceClass::Connections,
319                ResourceLimit::new(self.max_connections, "runtime.resources.maxConnections"),
320            ),
321            (
322                ResourceClass::BufferedBytes,
323                ResourceLimit::new(
324                    self.max_socket_buffered_bytes,
325                    "runtime.resources.maxSocketBufferedBytes",
326                ),
327            ),
328            (
329                ResourceClass::Datagrams,
330                ResourceLimit::new(self.max_datagrams, "runtime.resources.maxDatagrams"),
331            ),
332            (
333                ResourceClass::Timers,
334                ResourceLimit::new(self.max_timers, "runtime.resources.maxTimers"),
335            ),
336            (
337                ResourceClass::Tasks,
338                ResourceLimit::new(self.max_tasks, "runtime.resources.maxTasks"),
339            ),
340            (
341                ResourceClass::HandleCommands,
342                ResourceLimit::new(
343                    self.max_handle_commands,
344                    "runtime.resources.maxHandleCommands",
345                ),
346            ),
347            (
348                ResourceClass::HandleCommandBytes,
349                ResourceLimit::new(
350                    self.max_handle_command_bytes,
351                    "runtime.resources.maxHandleCommandBytes",
352                ),
353            ),
354            (
355                ResourceClass::BridgeCalls,
356                ResourceLimit::new(self.max_bridge_calls, "runtime.resources.maxBridgeCalls"),
357            ),
358            (
359                ResourceClass::BridgeRequestBytes,
360                ResourceLimit::new(
361                    self.max_bridge_request_bytes,
362                    "runtime.resources.maxBridgeRequestBytes",
363                ),
364            ),
365            (
366                ResourceClass::BridgeResponseBytes,
367                ResourceLimit::new(
368                    self.max_bridge_response_bytes,
369                    "runtime.resources.maxBridgeResponseBytes",
370                ),
371            ),
372            (
373                ResourceClass::AsyncCompletions,
374                ResourceLimit::new(
375                    self.max_async_completions,
376                    "runtime.resources.maxAsyncCompletions",
377                ),
378            ),
379            (
380                ResourceClass::AsyncCompletionBytes,
381                ResourceLimit::new(
382                    self.max_async_completion_bytes,
383                    "runtime.resources.maxAsyncCompletionBytes",
384                ),
385            ),
386            (
387                ResourceClass::UdpDatagrams,
388                ResourceLimit::new(self.max_udp_datagrams, "runtime.resources.maxUdpDatagrams"),
389            ),
390            (
391                ResourceClass::UdpBytes,
392                ResourceLimit::new(self.max_udp_bytes, "runtime.resources.maxUdpBytes"),
393            ),
394            (
395                ResourceClass::TlsBytes,
396                ResourceLimit::new(self.max_tls_bytes, "runtime.resources.maxTlsBytes"),
397            ),
398            (
399                ResourceClass::Http2Connections,
400                ResourceLimit::new(
401                    self.max_http2_connections,
402                    "runtime.resources.maxHttp2Connections",
403                ),
404            ),
405            (
406                ResourceClass::Http2Streams,
407                ResourceLimit::new(self.max_http2_streams, "runtime.resources.maxHttp2Streams"),
408            ),
409            (
410                ResourceClass::Http2BufferedBytes,
411                ResourceLimit::new(
412                    self.max_http2_buffered_bytes,
413                    "runtime.resources.maxHttp2BufferedBytes",
414                ),
415            ),
416            (
417                ResourceClass::Http2HeaderBytes,
418                ResourceLimit::new(
419                    self.max_http2_header_bytes,
420                    "runtime.resources.maxHttp2HeaderBytes",
421                ),
422            ),
423            (
424                ResourceClass::Http2DataBytes,
425                ResourceLimit::new(
426                    self.max_http2_data_bytes,
427                    "runtime.resources.maxHttp2DataBytes",
428                ),
429            ),
430            (
431                ResourceClass::Http2Commands,
432                ResourceLimit::new(
433                    self.max_http2_commands,
434                    "runtime.resources.maxHttp2Commands",
435                ),
436            ),
437            (
438                ResourceClass::Http2CommandBytes,
439                ResourceLimit::new(
440                    self.max_http2_command_bytes,
441                    "runtime.resources.maxHttp2CommandBytes",
442                ),
443            ),
444            (
445                ResourceClass::Http2Events,
446                ResourceLimit::new(self.max_http2_events, "runtime.resources.maxHttp2Events"),
447            ),
448            (
449                ResourceClass::Http2EventBytes,
450                ResourceLimit::new(
451                    self.max_http2_event_bytes,
452                    "runtime.resources.maxHttp2EventBytes",
453                ),
454            ),
455        ]
456    }
457}
458
459#[derive(Clone, Debug, PartialEq, Eq)]
460pub struct RuntimeConfig {
461    pub worker_threads: usize,
462    pub max_active_vm_executors: usize,
463    pub vm_executor_teardown_timeout_ms: u64,
464    pub blocking_worker_threads: usize,
465    pub max_blocking_jobs: usize,
466    pub max_queued_blocking_jobs: usize,
467    pub max_blocking_job_bytes: usize,
468    pub blocking_job_timeout_ms: u64,
469    pub task_poll_watchdog_ms: u64,
470    pub max_terminal_task_reports: usize,
471    pub protocol: RuntimeProtocolConfig,
472    pub resources: RuntimeResourceConfig,
473    pub fairness: RuntimeFairnessConfig,
474}
475
476impl Default for RuntimeConfig {
477    fn default() -> Self {
478        let available = thread::available_parallelism()
479            .map(usize::from)
480            .unwrap_or(1);
481        Self {
482            worker_threads: available.clamp(1, 4),
483            max_active_vm_executors: available.max(1),
484            vm_executor_teardown_timeout_ms: DEFAULT_VM_EXECUTOR_TEARDOWN_TIMEOUT_MS,
485            blocking_worker_threads: available.clamp(1, 4),
486            max_blocking_jobs: DEFAULT_MAX_BLOCKING_JOBS,
487            max_queued_blocking_jobs: DEFAULT_MAX_QUEUED_BLOCKING_JOBS,
488            max_blocking_job_bytes: DEFAULT_MAX_BLOCKING_JOB_BYTES,
489            blocking_job_timeout_ms: DEFAULT_BLOCKING_JOB_TIMEOUT_MS,
490            task_poll_watchdog_ms: DEFAULT_TASK_POLL_WATCHDOG_MS,
491            max_terminal_task_reports: DEFAULT_MAX_TERMINAL_TASK_REPORTS,
492            protocol: RuntimeProtocolConfig::default(),
493            resources: RuntimeResourceConfig::default(),
494            fairness: RuntimeFairnessConfig::default(),
495        }
496    }
497}
498
499impl RuntimeConfig {
500    pub fn validate(&self) -> Result<(), RuntimeBuildError> {
501        for (field, value) in [
502            ("runtime.workerThreads", self.worker_threads),
503            (
504                "runtime.executor.maxActiveVms",
505                self.max_active_vm_executors,
506            ),
507            (
508                "runtime.blocking.workerThreads",
509                self.blocking_worker_threads,
510            ),
511            ("runtime.blocking.maxJobs", self.max_blocking_jobs),
512            (
513                "runtime.blocking.maxQueuedJobs",
514                self.max_queued_blocking_jobs,
515            ),
516            (
517                "runtime.blocking.maxQueuedBytes",
518                self.max_blocking_job_bytes,
519            ),
520            (
521                "runtime.tasks.maxTerminalReports",
522                self.max_terminal_task_reports,
523            ),
524            (
525                "runtime.protocol.maxIngressFrames",
526                self.protocol.max_ingress_frames,
527            ),
528            (
529                "runtime.protocol.maxIngressBytes",
530                self.protocol.max_ingress_bytes,
531            ),
532            (
533                "runtime.protocol.maxControlFrames",
534                self.protocol.max_control_frames,
535            ),
536            (
537                "runtime.protocol.maxControlBytes",
538                self.protocol.max_control_bytes,
539            ),
540            (
541                "runtime.protocol.maxEgressFrames",
542                self.protocol.max_egress_frames,
543            ),
544            (
545                "runtime.protocol.maxEgressBytes",
546                self.protocol.max_egress_bytes,
547            ),
548            (
549                "runtime.protocol.maxInFlightRequests",
550                self.protocol.max_in_flight_requests,
551            ),
552            (
553                "runtime.protocol.maxInFlightRequestBytes",
554                self.protocol.max_in_flight_request_bytes,
555            ),
556            (
557                "runtime.protocol.maxTerminalFrames",
558                self.protocol.max_terminal_frames,
559            ),
560            (
561                "runtime.protocol.maxTerminalBytes",
562                self.protocol.max_terminal_bytes,
563            ),
564            (
565                "runtime.protocol.terminalFallbackBytes",
566                self.protocol.terminal_fallback_bytes,
567            ),
568            (
569                "runtime.protocol.maxProgressFrames",
570                self.protocol.max_progress_frames,
571            ),
572            (
573                "runtime.protocol.maxProgressBytes",
574                self.protocol.max_progress_bytes,
575            ),
576            (
577                "runtime.protocol.maxRejectionFrames",
578                self.protocol.max_rejection_frames,
579            ),
580            (
581                "runtime.protocol.maxRejectionBytes",
582                self.protocol.max_rejection_bytes,
583            ),
584            (
585                "runtime.protocol.maxPendingResponses",
586                self.protocol.max_pending_responses,
587            ),
588            (
589                "runtime.protocol.maxSessionsPerConnection",
590                self.protocol.max_sessions_per_connection,
591            ),
592            (
593                "runtime.protocol.maxPendingResponseBytes",
594                self.protocol.max_pending_response_bytes,
595            ),
596            (
597                "runtime.protocol.maxProcessEvents",
598                self.protocol.max_process_events,
599            ),
600            (
601                "runtime.protocol.maxOutboundRequests",
602                self.protocol.max_outbound_requests,
603            ),
604            (
605                "runtime.protocol.maxCompletedResponses",
606                self.protocol.max_completed_responses,
607            ),
608            (
609                "runtime.resources.maxCapabilities",
610                self.resources.max_capabilities,
611            ),
612            (
613                "runtime.resources.maxReadyHandles",
614                self.resources.max_ready_handles,
615            ),
616            ("runtime.resources.maxSockets", self.resources.max_sockets),
617            (
618                "runtime.resources.maxConnections",
619                self.resources.max_connections,
620            ),
621            (
622                "runtime.resources.maxSocketBufferedBytes",
623                self.resources.max_socket_buffered_bytes,
624            ),
625            (
626                "runtime.resources.maxDatagrams",
627                self.resources.max_datagrams,
628            ),
629            ("runtime.resources.maxTimers", self.resources.max_timers),
630            ("runtime.resources.maxTasks", self.resources.max_tasks),
631            (
632                "runtime.resources.maxHandleCommands",
633                self.resources.max_handle_commands,
634            ),
635            (
636                "runtime.resources.maxHandleCommandBytes",
637                self.resources.max_handle_command_bytes,
638            ),
639            (
640                "runtime.resources.maxBridgeCalls",
641                self.resources.max_bridge_calls,
642            ),
643            (
644                "runtime.resources.maxBridgeRequestBytes",
645                self.resources.max_bridge_request_bytes,
646            ),
647            (
648                "runtime.resources.maxBridgeResponseBytes",
649                self.resources.max_bridge_response_bytes,
650            ),
651            (
652                "runtime.resources.maxAsyncCompletions",
653                self.resources.max_async_completions,
654            ),
655            (
656                "runtime.resources.maxAsyncCompletionBytes",
657                self.resources.max_async_completion_bytes,
658            ),
659            (
660                "runtime.resources.maxUdpDatagrams",
661                self.resources.max_udp_datagrams,
662            ),
663            (
664                "runtime.resources.maxUdpBytes",
665                self.resources.max_udp_bytes,
666            ),
667            (
668                "runtime.resources.maxTlsBytes",
669                self.resources.max_tls_bytes,
670            ),
671            (
672                "runtime.resources.maxHttp2Connections",
673                self.resources.max_http2_connections,
674            ),
675            (
676                "runtime.resources.maxHttp2Streams",
677                self.resources.max_http2_streams,
678            ),
679            (
680                "runtime.resources.maxHttp2BufferedBytes",
681                self.resources.max_http2_buffered_bytes,
682            ),
683            (
684                "runtime.resources.maxHttp2HeaderBytes",
685                self.resources.max_http2_header_bytes,
686            ),
687            (
688                "runtime.resources.maxHttp2DataBytes",
689                self.resources.max_http2_data_bytes,
690            ),
691            (
692                "runtime.resources.maxHttp2Commands",
693                self.resources.max_http2_commands,
694            ),
695            (
696                "runtime.resources.maxHttp2CommandBytes",
697                self.resources.max_http2_command_bytes,
698            ),
699            (
700                "runtime.resources.maxHttp2Events",
701                self.resources.max_http2_events,
702            ),
703            (
704                "runtime.resources.maxHttp2EventBytes",
705                self.resources.max_http2_event_bytes,
706            ),
707        ] {
708            if value == 0 {
709                return Err(RuntimeBuildError(format!(
710                    "ERR_AGENTOS_RUNTIME_CONFIG: {field} must be greater than zero"
711                )));
712            }
713        }
714        if self.task_poll_watchdog_ms == 0 {
715            return Err(RuntimeBuildError(String::from(
716                "ERR_AGENTOS_RUNTIME_CONFIG: runtime.watchdog.taskPollMs must be greater than zero",
717            )));
718        }
719        if self.vm_executor_teardown_timeout_ms == 0 {
720            return Err(RuntimeBuildError(String::from(
721                "ERR_AGENTOS_RUNTIME_CONFIG: runtime.executor.teardownTimeoutMs must be greater than zero",
722            )));
723        }
724        if self.blocking_job_timeout_ms == 0 {
725            return Err(RuntimeBuildError(String::from(
726                "ERR_AGENTOS_RUNTIME_CONFIG: runtime.blocking.jobTimeoutMs must be greater than zero",
727            )));
728        }
729        if self.protocol.shutdown_grace_ms == 0 {
730            return Err(RuntimeBuildError(String::from(
731                "ERR_AGENTOS_RUNTIME_CONFIG: runtime.protocol.shutdownGraceMs must be greater than zero",
732            )));
733        }
734        if self.protocol.max_terminal_frames < self.protocol.max_in_flight_requests {
735            return Err(RuntimeBuildError(format!(
736                "ERR_AGENTOS_RUNTIME_CONFIG: runtime.protocol.maxTerminalFrames ({}) must be >= runtime.protocol.maxInFlightRequests ({}); raise runtime.protocol.maxTerminalFrames",
737                self.protocol.max_terminal_frames, self.protocol.max_in_flight_requests
738            )));
739        }
740        let required_terminal_bytes = self
741            .protocol
742            .max_in_flight_requests
743            .checked_mul(self.protocol.terminal_fallback_bytes)
744            .ok_or_else(|| {
745                RuntimeBuildError(String::from(
746                    "ERR_AGENTOS_RUNTIME_CONFIG: runtime.protocol.maxInFlightRequests * runtime.protocol.terminalFallbackBytes overflows usize; lower either value",
747                ))
748            })?;
749        if self.protocol.max_terminal_bytes < required_terminal_bytes {
750            return Err(RuntimeBuildError(format!(
751                "ERR_AGENTOS_RUNTIME_CONFIG: runtime.protocol.maxTerminalBytes ({}) must be >= runtime.protocol.maxInFlightRequests * runtime.protocol.terminalFallbackBytes ({}); raise runtime.protocol.maxTerminalBytes",
752                self.protocol.max_terminal_bytes, required_terminal_bytes
753            )));
754        }
755        let logical_control_frames = self
756            .protocol
757            .max_terminal_frames
758            .checked_add(self.protocol.max_progress_frames)
759            .and_then(|value| value.checked_add(self.protocol.max_rejection_frames))
760            .ok_or_else(|| {
761                RuntimeBuildError(String::from(
762                    "ERR_AGENTOS_RUNTIME_CONFIG: protocol logical control frame capacities overflow usize; lower runtime.protocol.maxTerminalFrames, maxProgressFrames, or maxRejectionFrames",
763                ))
764            })?;
765        if logical_control_frames > self.protocol.max_control_frames {
766            return Err(RuntimeBuildError(format!(
767                "ERR_AGENTOS_RUNTIME_CONFIG: protocol logical control frames ({logical_control_frames}) exceed runtime.protocol.maxControlFrames ({}); raise runtime.protocol.maxControlFrames or lower the logical lane limits",
768                self.protocol.max_control_frames
769            )));
770        }
771        let logical_control_bytes = self
772            .protocol
773            .max_terminal_bytes
774            .checked_add(self.protocol.max_progress_bytes)
775            .and_then(|value| value.checked_add(self.protocol.max_rejection_bytes))
776            .ok_or_else(|| {
777                RuntimeBuildError(String::from(
778                    "ERR_AGENTOS_RUNTIME_CONFIG: protocol logical control byte capacities overflow usize; lower runtime.protocol.maxTerminalBytes, maxProgressBytes, or maxRejectionBytes",
779                ))
780            })?;
781        if logical_control_bytes > self.protocol.max_control_bytes {
782            return Err(RuntimeBuildError(format!(
783                "ERR_AGENTOS_RUNTIME_CONFIG: protocol logical control bytes ({logical_control_bytes}) exceed runtime.protocol.maxControlBytes ({}); raise runtime.protocol.maxControlBytes or lower the logical lane limits",
784                self.protocol.max_control_bytes
785            )));
786        }
787        for (field, value) in [
788            (
789                "runtime.fairness.vmQuantumOperations",
790                self.fairness.vm_quantum_operations,
791            ),
792            (
793                "runtime.fairness.vmQuantumBytes",
794                self.fairness.vm_quantum_bytes,
795            ),
796            (
797                "runtime.fairness.capabilityQuantumOperations",
798                self.fairness.capability_quantum_operations,
799            ),
800            (
801                "runtime.fairness.capabilityQuantumBytes",
802                self.fairness.capability_quantum_bytes,
803            ),
804            (
805                "runtime.fairness.maxVmDeficitOperations",
806                self.fairness.max_vm_deficit_operations,
807            ),
808            (
809                "runtime.fairness.maxVmDeficitBytes",
810                self.fairness.max_vm_deficit_bytes,
811            ),
812            (
813                "runtime.fairness.maxCapabilityDeficitOperations",
814                self.fairness.max_capability_deficit_operations,
815            ),
816            (
817                "runtime.fairness.maxCapabilityDeficitBytes",
818                self.fairness.max_capability_deficit_bytes,
819            ),
820            ("runtime.fairness.maxVms", self.fairness.max_vms),
821            (
822                "runtime.fairness.maxCapabilitiesPerVm",
823                self.fairness.max_capabilities_per_vm,
824            ),
825        ] {
826            if value == 0 {
827                return Err(RuntimeBuildError(format!(
828                    "ERR_AGENTOS_RUNTIME_CONFIG: {field} must be greater than zero"
829                )));
830            }
831        }
832        if self.fairness.max_vm_deficit_operations < self.fairness.vm_quantum_operations
833            || self.fairness.max_vm_deficit_bytes < self.fairness.vm_quantum_bytes
834            || self.fairness.max_capability_deficit_operations
835                < self.fairness.capability_quantum_operations
836            || self.fairness.max_capability_deficit_bytes < self.fairness.capability_quantum_bytes
837        {
838            return Err(RuntimeBuildError(String::from(
839                "ERR_AGENTOS_RUNTIME_CONFIG: fairness deficits must be at least their quantum",
840            )));
841        }
842        if self.max_blocking_jobs < self.blocking_worker_threads {
843            return Err(RuntimeBuildError(format!(
844                "ERR_AGENTOS_RUNTIME_CONFIG: runtime.blocking.maxJobs ({}) must be >= runtime.blocking.workerThreads ({})",
845                self.max_blocking_jobs, self.blocking_worker_threads
846            )));
847        }
848        if self.max_queued_blocking_jobs > self.max_blocking_jobs {
849            return Err(RuntimeBuildError(format!(
850                "ERR_AGENTOS_RUNTIME_CONFIG: runtime.blocking.maxQueuedJobs ({}) must be <= runtime.blocking.maxJobs ({})",
851                self.max_queued_blocking_jobs, self.max_blocking_jobs
852            )));
853        }
854        Ok(())
855    }
856}
857
858#[derive(Clone, Debug, PartialEq, Eq)]
859pub struct RuntimeBuildError(String);
860
861impl fmt::Display for RuntimeBuildError {
862    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
863        formatter.write_str(&self.0)
864    }
865}
866
867impl std::error::Error for RuntimeBuildError {}
868
869#[derive(Debug, Clone, PartialEq, Eq)]
870pub enum BlockingJobError {
871    ResourceLimit(LimitError),
872    Capacity { limit: usize },
873    ShuttingDown,
874    WorkerDropped,
875    TimedOut { timeout: Duration },
876}
877
878impl fmt::Display for BlockingJobError {
879    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
880        match self {
881            Self::ResourceLimit(error) => error.fmt(formatter),
882            Self::Capacity { limit } => write!(
883                formatter,
884                "ERR_AGENTOS_BLOCKING_JOB_LIMIT: blocking executor queue exceeded {limit} jobs; raise runtime.blocking.maxQueuedJobs"
885            ),
886            Self::ShuttingDown => formatter.write_str(
887                "ERR_AGENTOS_BLOCKING_EXECUTOR_SHUTDOWN: blocking executor is shutting down",
888            ),
889            Self::WorkerDropped => formatter.write_str(
890                "ERR_AGENTOS_BLOCKING_WORKER_DROPPED: blocking worker ended without a result",
891            ),
892            Self::TimedOut { timeout } => write!(
893                formatter,
894                "ERR_AGENTOS_BLOCKING_JOB_TIMEOUT: blocking job exceeded its {}ms deadline",
895                timeout.as_millis()
896            ),
897        }
898    }
899}
900
901impl std::error::Error for BlockingJobError {}
902
903type BlockingOperation = Box<dyn FnOnce(Reservation, Reservation) + Send + 'static>;
904
905struct BlockingJob {
906    operation: BlockingOperation,
907    _slot: Reservation,
908    _bytes: Reservation,
909}
910
911struct BlockingExecutorState {
912    metrics: RuntimeMetrics,
913    queued: AtomicUsize,
914    active: AtomicUsize,
915}
916
917struct BlockingExecutorInner {
918    sender: Mutex<Option<mpsc::SyncSender<BlockingJob>>>,
919    workers: Mutex<Vec<thread::JoinHandle<()>>>,
920    max_queued_jobs: usize,
921    max_bytes: usize,
922    state: Arc<BlockingExecutorState>,
923}
924
925impl Drop for BlockingExecutorInner {
926    fn drop(&mut self) {
927        self.sender.get_mut().ok().and_then(Option::take);
928        let workers = self
929            .workers
930            .get_mut()
931            .map(std::mem::take)
932            .unwrap_or_default();
933        for worker in workers {
934            if worker.join().is_err() {
935                eprintln!("ERR_AGENTOS_BLOCKING_WORKER_PANIC: blocking executor worker panicked");
936            }
937        }
938    }
939}
940
941#[derive(Clone)]
942pub struct BlockingExecutor {
943    inner: Arc<BlockingExecutorInner>,
944    resources: Arc<ResourceLedger>,
945    admission_open: Arc<AtomicBool>,
946    admission_gate: Arc<Mutex<()>>,
947}
948
949impl fmt::Debug for BlockingExecutor {
950    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
951        formatter
952            .debug_struct("BlockingExecutor")
953            .field("worker_count", &self.worker_count())
954            .field("max_queued_jobs", &self.inner.max_queued_jobs)
955            .field("max_bytes", &self.inner.max_bytes)
956            .field("reserved_bytes", &self.reserved_bytes())
957            .finish()
958    }
959}
960
961impl BlockingExecutor {
962    fn new(
963        config: &RuntimeConfig,
964        resources: Arc<ResourceLedger>,
965        metrics: RuntimeMetrics,
966        admission_open: Arc<AtomicBool>,
967        admission_gate: Arc<Mutex<()>>,
968    ) -> Result<Self, RuntimeBuildError> {
969        let (sender, receiver) = mpsc::sync_channel::<BlockingJob>(config.max_queued_blocking_jobs);
970        let receiver = Arc::new(Mutex::new(receiver));
971        let executor_state = Arc::new(BlockingExecutorState {
972            metrics,
973            queued: AtomicUsize::new(0),
974            active: AtomicUsize::new(0),
975        });
976        let mut workers = Vec::with_capacity(config.blocking_worker_threads);
977        for index in 0..config.blocking_worker_threads {
978            let receiver = Arc::clone(&receiver);
979            let executor_state = Arc::clone(&executor_state);
980            // AGENTOS_THREAD_SITE: blocking-executor-worker
981            let worker = thread::Builder::new()
982                .name(format!("agentos-blocking-{index}"))
983                .spawn(move || loop {
984                    let job = match receiver.lock() {
985                        Ok(receiver) => receiver.recv(),
986                        Err(_) => {
987                            eprintln!(
988                                "ERR_AGENTOS_BLOCKING_QUEUE_POISONED: blocking job receiver lock poisoned"
989                            );
990                            break;
991                        }
992                    };
993                    match job {
994                        Ok(job) => {
995                            decrement_saturating(&executor_state.queued);
996                            executor_state.active.fetch_add(1, Ordering::Relaxed);
997                            executor_state.metrics.observe_executor(
998                                ExecutorMetricClass::Blocking,
999                                executor_state.active.load(Ordering::Relaxed),
1000                                executor_state.queued.load(Ordering::Relaxed),
1001                            );
1002                            let BlockingJob {
1003                                operation,
1004                                _slot,
1005                                _bytes,
1006                            } = job;
1007                            if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1008                                operation(_slot, _bytes)
1009                            }))
1010                            .is_err()
1011                            {
1012                                eprintln!(
1013                                    "ERR_AGENTOS_BLOCKING_JOB_PANIC: blocking job panicked"
1014                                );
1015                            }
1016                            decrement_saturating(&executor_state.active);
1017                            executor_state.metrics.observe_executor(
1018                                ExecutorMetricClass::Blocking,
1019                                executor_state.active.load(Ordering::Relaxed),
1020                                executor_state.queued.load(Ordering::Relaxed),
1021                            );
1022                        }
1023                        Err(_) => break,
1024                    }
1025                })
1026                .map_err(|error| {
1027                    RuntimeBuildError(format!(
1028                        "ERR_AGENTOS_BLOCKING_WORKER_START: failed to start blocking worker {index}: {error}"
1029                    ))
1030                })?;
1031            workers.push(worker);
1032        }
1033
1034        Ok(Self {
1035            inner: Arc::new(BlockingExecutorInner {
1036                sender: Mutex::new(Some(sender)),
1037                workers: Mutex::new(workers),
1038                max_queued_jobs: config.max_queued_blocking_jobs,
1039                max_bytes: config.max_blocking_job_bytes,
1040                state: executor_state,
1041            }),
1042            resources,
1043            admission_open,
1044            admission_gate,
1045        })
1046    }
1047
1048    pub fn worker_count(&self) -> usize {
1049        self.inner
1050            .workers
1051            .lock()
1052            .map(|workers| workers.len())
1053            .unwrap_or(0)
1054    }
1055
1056    pub fn reserved_bytes(&self) -> usize {
1057        self.resources.usage(ResourceClass::ExecutorBytes).used
1058    }
1059
1060    /// Share the fixed workers and queue while charging admission to a child
1061    /// ledger (which atomically charges its process parent too).
1062    pub fn scoped(
1063        &self,
1064        resources: Arc<ResourceLedger>,
1065        admission_open: Arc<AtomicBool>,
1066        admission_gate: Arc<Mutex<()>>,
1067    ) -> Self {
1068        Self {
1069            inner: Arc::clone(&self.inner),
1070            resources,
1071            admission_open,
1072            admission_gate,
1073        }
1074    }
1075
1076    pub fn submit<F>(&self, reserved_bytes: usize, operation: F) -> Result<(), BlockingJobError>
1077    where
1078        F: FnOnce() + Send + 'static,
1079    {
1080        let (admission, _slot, _bytes) = self.reserve(reserved_bytes)?;
1081        let job = BlockingJob {
1082            operation: Box::new(move |_slot, _bytes| operation()),
1083            _slot,
1084            _bytes,
1085        };
1086        let result = self.try_enqueue(job);
1087        drop(admission);
1088        result
1089    }
1090
1091    pub async fn run<T, F>(
1092        &self,
1093        reserved_bytes: usize,
1094        operation: F,
1095    ) -> Result<T, BlockingJobError>
1096    where
1097        T: Send + 'static,
1098        F: FnOnce() -> T + Send + 'static,
1099    {
1100        let (admission, _slot, _bytes) = self.reserve(reserved_bytes)?;
1101        let (result_tx, result_rx) = tokio::sync::oneshot::channel();
1102        let job = BlockingJob {
1103            operation: Box::new(move |slot, bytes| {
1104                let result = operation();
1105                // Publish completion only after accounting is reconciled. A
1106                // caller observing the result must never still see the job's
1107                // reservations charged.
1108                drop(slot);
1109                drop(bytes);
1110                if result_tx.send(result).is_err() {
1111                    eprintln!(
1112                        "ERR_AGENTOS_BLOCKING_RESULT_DROPPED: asynchronous caller stopped waiting"
1113                    );
1114                }
1115            }),
1116            _slot,
1117            _bytes,
1118        };
1119
1120        let enqueue_result = self.try_enqueue(job);
1121        drop(admission);
1122        enqueue_result?;
1123
1124        result_rx.await.map_err(|_| BlockingJobError::WorkerDropped)
1125    }
1126
1127    pub fn run_sync<T, F>(
1128        &self,
1129        reserved_bytes: usize,
1130        timeout: Duration,
1131        operation: F,
1132    ) -> Result<T, BlockingJobError>
1133    where
1134        T: Send + 'static,
1135        F: FnOnce() -> T + Send + 'static,
1136    {
1137        let (admission, _slot, _bytes) = self.reserve(reserved_bytes)?;
1138        let (result_tx, result_rx) = mpsc::sync_channel(1);
1139        let job = BlockingJob {
1140            operation: Box::new(move |slot, bytes| {
1141                let result = operation();
1142                // Synchronous callers can run immediately after recv returns,
1143                // so release admission before making that result observable.
1144                drop(slot);
1145                drop(bytes);
1146                if result_tx.send(result).is_err() {
1147                    eprintln!(
1148                        "ERR_AGENTOS_BLOCKING_RESULT_DROPPED: synchronous caller stopped waiting"
1149                    );
1150                }
1151            }),
1152            _slot,
1153            _bytes,
1154        };
1155
1156        let enqueue_result = self.try_enqueue(job);
1157        drop(admission);
1158        enqueue_result?;
1159
1160        result_rx
1161            .recv_timeout(timeout)
1162            .map_err(|error| match error {
1163                mpsc::RecvTimeoutError::Timeout => BlockingJobError::TimedOut { timeout },
1164                mpsc::RecvTimeoutError::Disconnected => BlockingJobError::WorkerDropped,
1165            })
1166    }
1167
1168    fn reserve(
1169        &self,
1170        requested: usize,
1171    ) -> Result<(std::sync::MutexGuard<'_, ()>, Reservation, Reservation), BlockingJobError> {
1172        // Keep admission open through the queue insertion. Close uses the same
1173        // gate, so a stale clone cannot enqueue blocking work after close
1174        // returns and teardown has begun reconciling the VM ledger.
1175        let admission = self
1176            .admission_gate
1177            .lock()
1178            .map_err(|_| BlockingJobError::ShuttingDown)?;
1179        if !self.admission_open.load(Ordering::Acquire) {
1180            return Err(BlockingJobError::ShuttingDown);
1181        }
1182        let slot = self
1183            .resources
1184            .reserve(ResourceClass::ExecutorSlots, 1)
1185            .map_err(BlockingJobError::ResourceLimit)?;
1186        let bytes = self
1187            .resources
1188            .reserve(ResourceClass::ExecutorBytes, requested)
1189            .map_err(BlockingJobError::ResourceLimit)?;
1190        Ok((admission, slot, bytes))
1191    }
1192
1193    fn try_enqueue(&self, job: BlockingJob) -> Result<(), BlockingJobError> {
1194        let sender = self
1195            .inner
1196            .sender
1197            .lock()
1198            .map_err(|_| BlockingJobError::ShuttingDown)?;
1199        let sender = sender.as_ref().ok_or(BlockingJobError::ShuttingDown)?;
1200        self.record_enqueued();
1201        if let Err(error) = sender.try_send(job) {
1202            self.record_enqueue_failed();
1203            return Err(match error {
1204                mpsc::TrySendError::Full(_) => BlockingJobError::Capacity {
1205                    limit: self.inner.max_queued_jobs,
1206                },
1207                mpsc::TrySendError::Disconnected(_) => BlockingJobError::ShuttingDown,
1208            });
1209        }
1210        Ok(())
1211    }
1212
1213    fn record_enqueued(&self) {
1214        self.inner.state.queued.fetch_add(1, Ordering::Relaxed);
1215        self.observe_executor();
1216    }
1217
1218    fn record_enqueue_failed(&self) {
1219        decrement_saturating(&self.inner.state.queued);
1220        self.observe_executor();
1221    }
1222
1223    fn observe_executor(&self) {
1224        self.inner.state.metrics.observe_executor(
1225            ExecutorMetricClass::Blocking,
1226            self.inner.state.active.load(Ordering::Relaxed),
1227            self.inner.state.queued.load(Ordering::Relaxed),
1228        );
1229    }
1230}
1231
1232fn decrement_saturating(counter: &AtomicUsize) {
1233    let mut current = counter.load(Ordering::Relaxed);
1234    while current != 0 {
1235        match counter.compare_exchange_weak(
1236            current,
1237            current - 1,
1238            Ordering::Relaxed,
1239            Ordering::Relaxed,
1240        ) {
1241            Ok(_) => return,
1242            Err(observed) => current = observed,
1243        }
1244    }
1245}
1246
1247#[derive(Clone, Debug)]
1248pub struct RuntimeContext {
1249    handle: tokio::runtime::Handle,
1250    blocking: BlockingExecutor,
1251    resources: Arc<ResourceLedger>,
1252    tasks: TaskSupervisor,
1253    metrics: RuntimeMetrics,
1254    fairness: FairWorkBroker,
1255    terminal_failure: Arc<Mutex<Option<TaskTerminalReport>>>,
1256    task_poll_watchdog: Duration,
1257    max_active_vm_executors: usize,
1258    vm_executor_teardown_timeout: Duration,
1259    blocking_job_timeout: Duration,
1260    admission_open: Arc<AtomicBool>,
1261    admission_closed: Arc<tokio::sync::Notify>,
1262    next_vm_generation: Arc<AtomicU64>,
1263    default_owner: TaskOwner,
1264}
1265
1266impl RuntimeContext {
1267    pub fn handle(&self) -> &tokio::runtime::Handle {
1268        &self.handle
1269    }
1270
1271    pub fn blocking(&self) -> &BlockingExecutor {
1272        &self.blocking
1273    }
1274
1275    /// Aggregate process accountant shared by every VM and trusted subsystem.
1276    pub fn resources(&self) -> &Arc<ResourceLedger> {
1277        &self.resources
1278    }
1279
1280    pub fn tasks(&self) -> &TaskSupervisor {
1281        &self.tasks
1282    }
1283
1284    pub fn metrics(&self) -> &RuntimeMetrics {
1285        &self.metrics
1286    }
1287
1288    pub fn max_active_vm_executors(&self) -> usize {
1289        self.max_active_vm_executors
1290    }
1291
1292    pub fn vm_executor_teardown_timeout(&self) -> Duration {
1293        self.vm_executor_teardown_timeout
1294    }
1295
1296    pub fn blocking_job_timeout(&self) -> Duration {
1297        self.blocking_job_timeout
1298    }
1299
1300    pub fn fairness(&self) -> &FairWorkBroker {
1301        &self.fairness
1302    }
1303
1304    /// Allocate an identity in the same process-wide namespace as the shared
1305    /// fairness broker. Every sidecar facade using this runtime must draw from
1306    /// this counter; per-facade counters can collide after an earlier VM retires.
1307    pub fn allocate_vm_generation(&self) -> Result<u64, RuntimeBuildError> {
1308        self.next_vm_generation
1309            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
1310                current.checked_add(1)
1311            })
1312            .map(|previous| previous + 1)
1313            .map_err(|_| {
1314                RuntimeBuildError(String::from(
1315                    "ERR_AGENTOS_VM_GENERATION_EXHAUSTED: process VM generation counter overflowed",
1316                ))
1317            })
1318    }
1319
1320    /// VM generation bound to this admission scope, when this is a VM-scoped
1321    /// context. Fairness keys use this generation rather than a capability's
1322    /// per-registry generation, which restarts for every VM.
1323    pub fn vm_generation(&self) -> Option<u64> {
1324        match &self.default_owner {
1325            TaskOwner::Vm { generation } => Some(*generation),
1326            _ => None,
1327        }
1328    }
1329
1330    /// First failed or panicked task in this process/VM admission scope.
1331    /// This is a durable lifecycle latch, not a drainable telemetry queue.
1332    pub fn terminal_failure(&self) -> Option<TaskTerminalReport> {
1333        self.terminal_failure
1334            .lock()
1335            .map(|failure| failure.clone())
1336            .unwrap_or_else(|_| {
1337                eprintln!(
1338                    "ERR_AGENTOS_TASK_FAILURE_LATCH_POISONED: scope={}",
1339                    self.resources.scope()
1340                );
1341                Some(TaskTerminalReport {
1342                    class: TaskClass::Runtime,
1343                    owner: self.default_owner.clone(),
1344                    scope: self.resources.scope().to_owned(),
1345                    reason: TaskTerminalReason::Panicked,
1346                })
1347            })
1348    }
1349
1350    /// Permanently close task and blocking-job admission for this context
1351    /// scope and every clone of it. Existing admitted work retains ownership
1352    /// until its normal terminal path releases accounting.
1353    pub fn close_admission(&self) {
1354        // The supervisor owns the gate shared by task and blocking admission.
1355        // Closing through it linearizes this transition against both paths.
1356        self.tasks.close_admission();
1357        self.admission_closed.notify_waiters();
1358    }
1359
1360    pub fn admission_is_open(&self) -> bool {
1361        self.admission_open.load(Ordering::Acquire)
1362    }
1363
1364    /// Resolve when this admission scope begins teardown.
1365    ///
1366    /// The notification is armed before checking the atomic flag so a close
1367    /// between observation and await cannot be lost. Each VM-scoped context
1368    /// owns a distinct signal; closing one VM never cancels process or sibling
1369    /// VM work.
1370    pub async fn admission_closed(&self) {
1371        loop {
1372            let notified = self.admission_closed.notified();
1373            if !self.admission_is_open() {
1374                return;
1375            }
1376            notified.await;
1377        }
1378    }
1379
1380    /// Create a VM-scoped admission view without creating another runtime,
1381    /// scheduler, queue, or worker.
1382    pub fn scoped(&self, resources: Arc<ResourceLedger>) -> Self {
1383        let admission_open = Arc::new(AtomicBool::new(true));
1384        let admission_gate = Arc::new(Mutex::new(()));
1385        Self {
1386            handle: self.handle.clone(),
1387            blocking: self.blocking.scoped(
1388                Arc::clone(&resources),
1389                Arc::clone(&admission_open),
1390                Arc::clone(&admission_gate),
1391            ),
1392            resources: Arc::clone(&resources),
1393            tasks: self
1394                .tasks
1395                .scoped(resources, Arc::clone(&admission_open), admission_gate),
1396            metrics: self.metrics.clone(),
1397            fairness: self.fairness.clone(),
1398            terminal_failure: Arc::new(Mutex::new(None)),
1399            task_poll_watchdog: self.task_poll_watchdog,
1400            max_active_vm_executors: self.max_active_vm_executors,
1401            vm_executor_teardown_timeout: self.vm_executor_teardown_timeout,
1402            blocking_job_timeout: self.blocking_job_timeout,
1403            admission_open,
1404            admission_closed: Arc::new(tokio::sync::Notify::new()),
1405            next_vm_generation: Arc::clone(&self.next_vm_generation),
1406            default_owner: self.default_owner.clone(),
1407        }
1408    }
1409
1410    pub fn scoped_for_vm(&self, resources: Arc<ResourceLedger>, generation: u64) -> Self {
1411        let mut scoped = self.scoped(resources);
1412        scoped.default_owner = TaskOwner::Vm { generation };
1413        scoped
1414    }
1415
1416    pub fn spawn<F>(
1417        &self,
1418        class: TaskClass,
1419        future: F,
1420    ) -> Result<tokio::task::JoinHandle<F::Output>, TaskSpawnError>
1421    where
1422        F: Future + Send + 'static,
1423        F::Output: Send + 'static,
1424    {
1425        let failure_latch = Arc::clone(&self.terminal_failure);
1426        let handler = supervision::terminal_handler(move |report| {
1427            latch_terminal_failure(&failure_latch, report);
1428        });
1429        let mut guard = self
1430            .tasks
1431            .admit(class, self.default_owner.clone(), Some(handler))?;
1432        let future =
1433            WatchdogFuture::new(future, class, self.task_poll_watchdog, self.metrics.clone());
1434        Ok(self.handle.spawn(async move {
1435            let output = future.await;
1436            guard.complete();
1437            output
1438        }))
1439    }
1440
1441    pub fn spawn_result<F, T, E>(
1442        &self,
1443        class: TaskClass,
1444        future: F,
1445    ) -> Result<tokio::task::JoinHandle<Result<T, E>>, TaskSpawnError>
1446    where
1447        F: Future<Output = Result<T, E>> + Send + 'static,
1448        T: Send + 'static,
1449        E: Send + 'static,
1450    {
1451        let failure_latch = Arc::clone(&self.terminal_failure);
1452        let handler = supervision::terminal_handler(move |report| {
1453            latch_terminal_failure(&failure_latch, report);
1454        });
1455        let mut guard = self
1456            .tasks
1457            .admit(class, self.default_owner.clone(), Some(handler))?;
1458        let future =
1459            WatchdogFuture::new(future, class, self.task_poll_watchdog, self.metrics.clone());
1460        Ok(self.handle.spawn(async move {
1461            let output = future.await;
1462            if output.is_ok() {
1463                guard.complete();
1464            } else {
1465                guard.fail();
1466            }
1467            output
1468        }))
1469    }
1470
1471    /// Spawn work with an explicit lifecycle owner. The terminal hook runs for
1472    /// every exit reason after task accounting is reconciled and outside the
1473    /// supervisor lock, so it can fail/close the owner without lock inversion.
1474    pub fn spawn_owned<F, H>(
1475        &self,
1476        class: TaskClass,
1477        owner: TaskOwner,
1478        on_terminal: H,
1479        future: F,
1480    ) -> Result<tokio::task::JoinHandle<F::Output>, TaskSpawnError>
1481    where
1482        F: Future + Send + 'static,
1483        F::Output: Send + 'static,
1484        H: Fn(&TaskTerminalReport) + Send + Sync + 'static,
1485    {
1486        let failure_latch = Arc::clone(&self.terminal_failure);
1487        let handler = supervision::terminal_handler(move |report| {
1488            latch_terminal_failure(&failure_latch, report);
1489            on_terminal(report);
1490        });
1491        let mut guard = self.tasks.admit(class, owner, Some(handler))?;
1492        let future =
1493            WatchdogFuture::new(future, class, self.task_poll_watchdog, self.metrics.clone());
1494        Ok(self.handle.spawn(async move {
1495            let output = future.await;
1496            guard.complete();
1497            output
1498        }))
1499    }
1500
1501    pub fn spawn_owned_result<F, T, E, H>(
1502        &self,
1503        class: TaskClass,
1504        owner: TaskOwner,
1505        on_terminal: H,
1506        future: F,
1507    ) -> Result<tokio::task::JoinHandle<Result<T, E>>, TaskSpawnError>
1508    where
1509        F: Future<Output = Result<T, E>> + Send + 'static,
1510        T: Send + 'static,
1511        E: Send + 'static,
1512        H: Fn(&TaskTerminalReport) + Send + Sync + 'static,
1513    {
1514        let failure_latch = Arc::clone(&self.terminal_failure);
1515        let handler = supervision::terminal_handler(move |report| {
1516            latch_terminal_failure(&failure_latch, report);
1517            on_terminal(report);
1518        });
1519        let mut guard = self.tasks.admit(class, owner, Some(handler))?;
1520        let future =
1521            WatchdogFuture::new(future, class, self.task_poll_watchdog, self.metrics.clone());
1522        Ok(self.handle.spawn(async move {
1523            let output = future.await;
1524            if output.is_ok() {
1525                guard.complete();
1526            } else {
1527                guard.fail();
1528            }
1529            output
1530        }))
1531    }
1532}
1533
1534fn latch_terminal_failure(latch: &Mutex<Option<TaskTerminalReport>>, report: &TaskTerminalReport) {
1535    if !matches!(
1536        report.reason,
1537        TaskTerminalReason::Failed | TaskTerminalReason::Panicked
1538    ) {
1539        return;
1540    }
1541    let mut failure = latch.lock().unwrap_or_else(|poisoned| {
1542        eprintln!("ERR_AGENTOS_TASK_FAILURE_LATCH_POISONED: recovering terminal failure");
1543        poisoned.into_inner()
1544    });
1545    if failure.is_none() {
1546        *failure = Some(report.clone());
1547    }
1548}
1549
1550struct WatchdogFuture<F> {
1551    inner: Pin<Box<F>>,
1552    class: TaskClass,
1553    threshold: Duration,
1554    metrics: RuntimeMetrics,
1555    reported: bool,
1556}
1557
1558impl<F> WatchdogFuture<F> {
1559    fn new(future: F, class: TaskClass, threshold: Duration, metrics: RuntimeMetrics) -> Self {
1560        Self {
1561            inner: Box::pin(future),
1562            class,
1563            threshold,
1564            metrics,
1565            reported: false,
1566        }
1567    }
1568}
1569
1570impl<F: Future> Future for WatchdogFuture<F> {
1571    type Output = F::Output;
1572
1573    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
1574        let this = self.as_mut().get_mut();
1575        let started = Instant::now();
1576        let result = this.inner.as_mut().poll(context);
1577        let elapsed = started.elapsed();
1578        if elapsed >= this.threshold {
1579            this.metrics
1580                .record_watchdog(WatchdogMetric::LongTaskPoll, elapsed);
1581            if !this.reported {
1582                this.reported = true;
1583                let message = format!(
1584                    "task_class={:?} poll_ms={} threshold_ms={}",
1585                    this.class,
1586                    elapsed.as_millis(),
1587                    this.threshold.as_millis()
1588                );
1589                this.metrics.emit_stderr_fallback(TelemetryFallback {
1590                    severity: TelemetrySeverity::Warning,
1591                    code: TelemetryFallbackCode::RuntimeWorkerStall,
1592                    subsystem: TelemetrySubsystem::Runtime,
1593                    message: &message,
1594                });
1595            }
1596        }
1597        result
1598    }
1599}
1600
1601pub struct SidecarRuntime {
1602    config: RuntimeConfig,
1603    runtime: tokio::runtime::Runtime,
1604    context: RuntimeContext,
1605}
1606
1607static PROCESS_RUNTIME: OnceLock<Result<SidecarRuntime, RuntimeBuildError>> = OnceLock::new();
1608
1609impl SidecarRuntime {
1610    fn build(config: RuntimeConfig) -> Result<Self, RuntimeBuildError> {
1611        config.validate()?;
1612        let mut resource_limits = config.resources.limits();
1613        resource_limits.push((
1614            ResourceClass::ExecutorSlots,
1615            ResourceLimit::new(config.max_blocking_jobs, "runtime.blocking.maxJobs"),
1616        ));
1617        resource_limits.push((
1618            ResourceClass::ExecutorBytes,
1619            ResourceLimit::new(
1620                config.max_blocking_job_bytes,
1621                "runtime.blocking.maxQueuedBytes",
1622            ),
1623        ));
1624        let metrics = RuntimeMetrics::new();
1625        let fairness = FairWorkBroker::new(config.fairness.scheduler_config(), metrics.clone())
1626            .map_err(|error| {
1627                RuntimeBuildError(format!(
1628                    "ERR_AGENTOS_RUNTIME_FAIRNESS_START: failed to build process fairness broker: {error}"
1629                ))
1630            })?;
1631        let resources = Arc::new(ResourceLedger::root_with_metrics(
1632            "sidecar-process",
1633            resource_limits,
1634            metrics.clone(),
1635        ));
1636        let admission_open = Arc::new(AtomicBool::new(true));
1637        let admission_gate = Arc::new(Mutex::new(()));
1638        let blocking = BlockingExecutor::new(
1639            &config,
1640            Arc::clone(&resources),
1641            metrics.clone(),
1642            Arc::clone(&admission_open),
1643            Arc::clone(&admission_gate),
1644        )?;
1645        let runtime = tokio::runtime::Builder::new_multi_thread()
1646            .worker_threads(config.worker_threads)
1647            .thread_name_fn(|| {
1648                static NEXT_WORKER: AtomicUsize = AtomicUsize::new(0);
1649                format!(
1650                    "agentos-runtime-{}",
1651                    NEXT_WORKER.fetch_add(1, Ordering::Relaxed)
1652                )
1653            })
1654            .on_thread_start(|| IS_AGENTOS_RUNTIME_WORKER.with(|marker| marker.set(true)))
1655            .on_thread_stop(|| IS_AGENTOS_RUNTIME_WORKER.with(|marker| marker.set(false)))
1656            .enable_all()
1657            .build()
1658            .map_err(|error| {
1659                RuntimeBuildError(format!(
1660                    "ERR_AGENTOS_RUNTIME_START: failed to build process runtime: {error}"
1661                ))
1662            })?;
1663        let context = RuntimeContext {
1664            handle: runtime.handle().clone(),
1665            blocking,
1666            resources: Arc::clone(&resources),
1667            tasks: TaskSupervisor::new(
1668                resources,
1669                metrics.clone(),
1670                Arc::clone(&admission_open),
1671                admission_gate,
1672                config.max_terminal_task_reports,
1673            ),
1674            metrics,
1675            fairness,
1676            terminal_failure: Arc::new(Mutex::new(None)),
1677            task_poll_watchdog: Duration::from_millis(config.task_poll_watchdog_ms),
1678            max_active_vm_executors: config.max_active_vm_executors,
1679            vm_executor_teardown_timeout: Duration::from_millis(
1680                config.vm_executor_teardown_timeout_ms,
1681            ),
1682            blocking_job_timeout: Duration::from_millis(config.blocking_job_timeout_ms),
1683            admission_open,
1684            admission_closed: Arc::new(tokio::sync::Notify::new()),
1685            next_vm_generation: Arc::new(AtomicU64::new(0)),
1686            default_owner: TaskOwner::Process,
1687        };
1688        Ok(Self {
1689            config,
1690            runtime,
1691            context,
1692        })
1693    }
1694
1695    /// Return the one Tokio runtime owned by this sidecar process.
1696    ///
1697    /// The first caller fixes the process topology. A later caller requesting a
1698    /// different topology receives a typed configuration error instead of
1699    /// silently creating a subsystem- or VM-local runtime.
1700    pub fn process(config: &RuntimeConfig) -> Result<&'static Self, RuntimeBuildError> {
1701        match PROCESS_RUNTIME.get_or_init(|| Self::build(config.clone())) {
1702            Ok(runtime) if &runtime.config == config => Ok(runtime),
1703            Ok(runtime) => Err(RuntimeBuildError(format!(
1704                "ERR_AGENTOS_RUNTIME_ALREADY_CONFIGURED: process runtime uses {:?}, requested {:?}",
1705                runtime.config, config
1706            ))),
1707            Err(error) => Err(error.clone()),
1708        }
1709    }
1710
1711    /// Obtain the runtime already constructed by the process entrypoint.
1712    /// Subsystems must never silently select a default topology: doing so can
1713    /// race trusted configuration and make the first incidental caller own the
1714    /// process scheduler.
1715    pub fn process_context() -> Result<RuntimeContext, RuntimeBuildError> {
1716        match PROCESS_RUNTIME.get() {
1717            Some(Ok(runtime)) => Ok(runtime.context()),
1718            Some(Err(error)) => Err(error.clone()),
1719            #[cfg(test)]
1720            None => Self::process(&RuntimeConfig::default()).map(Self::context),
1721            #[cfg(not(test))]
1722            None => Err(RuntimeBuildError(String::from(
1723                "ERR_AGENTOS_RUNTIME_NOT_INITIALIZED: the process entrypoint must construct SidecarRuntime before starting subsystems",
1724            ))),
1725        }
1726    }
1727
1728    pub fn context(&self) -> RuntimeContext {
1729        self.context.clone()
1730    }
1731
1732    pub fn block_on<F: Future>(&self, future: F) -> F::Output {
1733        self.runtime.block_on(future)
1734    }
1735}
1736
1737#[cfg(test)]
1738mod tests {
1739    use super::*;
1740
1741    #[test]
1742    fn process_runtime_bounds_every_resource_class_by_default() {
1743        let runtime = SidecarRuntime::build(RuntimeConfig::default()).expect("build runtime");
1744        for resource in ResourceClass::ALL {
1745            let usage = runtime.context().resources().usage(resource);
1746            assert_eq!(usage.used, 0, "{} starts charged", resource.name());
1747            assert!(
1748                usage.limit.is_some_and(|limit| limit > 0),
1749                "{} has no positive process limit",
1750                resource.name()
1751            );
1752        }
1753    }
1754
1755    #[test]
1756    fn vm_generation_allocator_is_shared_by_scoped_contexts() {
1757        let runtime = SidecarRuntime::build(RuntimeConfig::default()).expect("build runtime");
1758        let process = runtime.context();
1759        let resources = Arc::new(ResourceLedger::root("vm-generation-test", []));
1760        let scoped = process.scoped(Arc::clone(&resources));
1761
1762        let first = process
1763            .allocate_vm_generation()
1764            .expect("allocate process generation");
1765        let second = scoped
1766            .allocate_vm_generation()
1767            .expect("allocate scoped generation");
1768
1769        assert_eq!(second, first + 1);
1770    }
1771
1772    #[test]
1773    fn validates_nonzero_runtime_limits() {
1774        let error = RuntimeConfig {
1775            worker_threads: 0,
1776            ..RuntimeConfig::default()
1777        }
1778        .validate()
1779        .expect_err("zero worker count must be rejected");
1780        assert!(error.to_string().contains("runtime.workerThreads"));
1781
1782        let error = RuntimeConfig {
1783            max_terminal_task_reports: 0,
1784            ..RuntimeConfig::default()
1785        }
1786        .validate()
1787        .expect_err("zero terminal-report capacity must be rejected");
1788        assert!(error
1789            .to_string()
1790            .contains("runtime.tasks.maxTerminalReports"));
1791
1792        let error = RuntimeConfig {
1793            max_active_vm_executors: 0,
1794            ..RuntimeConfig::default()
1795        }
1796        .validate()
1797        .expect_err("zero VM executor capacity must be rejected");
1798        assert!(error.to_string().contains("runtime.executor.maxActiveVms"));
1799
1800        let error = RuntimeConfig {
1801            vm_executor_teardown_timeout_ms: 0,
1802            ..RuntimeConfig::default()
1803        }
1804        .validate()
1805        .expect_err("zero VM executor teardown timeout must be rejected");
1806        assert!(error
1807            .to_string()
1808            .contains("runtime.executor.teardownTimeoutMs"));
1809
1810        let error = RuntimeConfig {
1811            blocking_job_timeout_ms: 0,
1812            ..RuntimeConfig::default()
1813        }
1814        .validate()
1815        .expect_err("zero blocking-job timeout must be rejected");
1816        assert!(error.to_string().contains("runtime.blocking.jobTimeoutMs"));
1817
1818        let error = RuntimeConfig {
1819            protocol: RuntimeProtocolConfig {
1820                max_ingress_bytes: 0,
1821                ..RuntimeProtocolConfig::default()
1822            },
1823            ..RuntimeConfig::default()
1824        }
1825        .validate()
1826        .expect_err("zero protocol byte capacity must be rejected");
1827        assert!(error
1828            .to_string()
1829            .contains("runtime.protocol.maxIngressBytes"));
1830    }
1831
1832    #[test]
1833    fn validates_request_and_output_protocol_capacities() {
1834        assert_eq!(
1835            RuntimeProtocolConfig::default().shutdown_grace_ms,
1836            DEFAULT_PROTOCOL_SHUTDOWN_GRACE_TIMEOUT_MS
1837        );
1838
1839        macro_rules! assert_zero_rejected {
1840            ($field:ident, $path:literal) => {{
1841                let mut config = RuntimeConfig::default();
1842                config.protocol.$field = 0;
1843                let error = config
1844                    .validate()
1845                    .expect_err(concat!(stringify!($field), " must be positive"));
1846                assert!(
1847                    error.to_string().contains($path),
1848                    "unexpected validation error for {}: {error}",
1849                    stringify!($field)
1850                );
1851            }};
1852        }
1853
1854        assert_zero_rejected!(max_in_flight_requests, "maxInFlightRequests");
1855        assert_zero_rejected!(max_in_flight_request_bytes, "maxInFlightRequestBytes");
1856        assert_zero_rejected!(max_sessions_per_connection, "maxSessionsPerConnection");
1857        assert_zero_rejected!(max_terminal_frames, "maxTerminalFrames");
1858        assert_zero_rejected!(max_terminal_bytes, "maxTerminalBytes");
1859        assert_zero_rejected!(terminal_fallback_bytes, "terminalFallbackBytes");
1860        assert_zero_rejected!(max_progress_frames, "maxProgressFrames");
1861        assert_zero_rejected!(max_progress_bytes, "maxProgressBytes");
1862        assert_zero_rejected!(max_rejection_frames, "maxRejectionFrames");
1863        assert_zero_rejected!(max_rejection_bytes, "maxRejectionBytes");
1864
1865        let mut config = RuntimeConfig::default();
1866        config.protocol.shutdown_grace_ms = 0;
1867        let error = config
1868            .validate()
1869            .expect_err("shutdown grace must be positive");
1870        assert!(
1871            error
1872                .to_string()
1873                .contains("runtime.protocol.shutdownGraceMs"),
1874            "unexpected validation error for shutdown_grace_ms: {error}"
1875        );
1876
1877        RuntimeConfig::default()
1878            .validate()
1879            .expect("default protocol capacities must be internally consistent");
1880    }
1881
1882    #[test]
1883    fn validates_terminal_reservations_and_logical_control_capacity() {
1884        let mut terminal_frames = RuntimeConfig::default();
1885        terminal_frames.protocol.max_terminal_frames =
1886            terminal_frames.protocol.max_in_flight_requests - 1;
1887        let error = terminal_frames
1888            .validate()
1889            .expect_err("every admitted request needs a terminal frame");
1890        assert!(error.to_string().contains("maxTerminalFrames"));
1891        assert!(error.to_string().contains("maxInFlightRequests"));
1892        assert!(error.to_string().contains("raise"));
1893
1894        let mut terminal_bytes = RuntimeConfig::default();
1895        let required = terminal_bytes.protocol.max_in_flight_requests
1896            * terminal_bytes.protocol.terminal_fallback_bytes;
1897        terminal_bytes.protocol.max_terminal_bytes = required - 1;
1898        let error = terminal_bytes
1899            .validate()
1900            .expect_err("every admitted request needs fallback terminal bytes");
1901        assert!(error.to_string().contains("maxTerminalBytes"));
1902        assert!(error.to_string().contains("terminalFallbackBytes"));
1903        assert!(error.to_string().contains("raise"));
1904
1905        let mut control_frames = RuntimeConfig::default();
1906        control_frames.protocol.max_control_frames = control_frames.protocol.max_terminal_frames
1907            + control_frames.protocol.max_progress_frames
1908            + control_frames.protocol.max_rejection_frames
1909            - 1;
1910        let error = control_frames
1911            .validate()
1912            .expect_err("logical frame lanes must fit physical control capacity");
1913        assert!(error.to_string().contains("maxControlFrames"));
1914
1915        let mut control_bytes = RuntimeConfig::default();
1916        control_bytes.protocol.max_control_bytes = control_bytes.protocol.max_terminal_bytes
1917            + control_bytes.protocol.max_progress_bytes
1918            + control_bytes.protocol.max_rejection_bytes
1919            - 1;
1920        let error = control_bytes
1921            .validate()
1922            .expect_err("logical byte lanes must fit physical control capacity");
1923        assert!(error.to_string().contains("maxControlBytes"));
1924    }
1925
1926    #[test]
1927    fn blocking_executor_enforces_byte_reservations() {
1928        let runtime = SidecarRuntime::build(RuntimeConfig {
1929            worker_threads: 1,
1930            blocking_worker_threads: 1,
1931            max_queued_blocking_jobs: 1,
1932            max_blocking_job_bytes: 8,
1933            ..RuntimeConfig::default()
1934        })
1935        .expect("build runtime");
1936        let blocking = runtime.context().blocking().clone();
1937        let error = runtime
1938            .block_on(blocking.run(9, || 1usize))
1939            .expect_err("oversize blocking job must be rejected");
1940        assert!(matches!(error, BlockingJobError::ResourceLimit(_)));
1941        assert_eq!(blocking.reserved_bytes(), 0);
1942    }
1943
1944    #[test]
1945    fn blocking_executor_runs_on_fixed_workers_and_releases_bytes() {
1946        let runtime = SidecarRuntime::build(RuntimeConfig {
1947            worker_threads: 1,
1948            blocking_worker_threads: 2,
1949            max_queued_blocking_jobs: 2,
1950            max_blocking_job_bytes: 32,
1951            ..RuntimeConfig::default()
1952        })
1953        .expect("build runtime");
1954        let blocking = runtime.context().blocking().clone();
1955        let worker_name = runtime
1956            .block_on(blocking.run(4, || {
1957                thread::current().name().unwrap_or_default().to_owned()
1958            }))
1959            .expect("blocking job result");
1960        assert!(worker_name.starts_with("agentos-blocking-"));
1961        assert_eq!(blocking.worker_count(), 2);
1962        assert_eq!(blocking.reserved_bytes(), 0);
1963        let metrics = runtime.context().metrics().snapshot();
1964        assert_eq!(
1965            metrics.buffers[metrics::BufferMetricClass::Executor.index()].current,
1966            0
1967        );
1968        assert_eq!(
1969            metrics.buffers[metrics::BufferMetricClass::Executor.index()].high_water,
1970            4
1971        );
1972        assert!(
1973            metrics.executors[ExecutorMetricClass::Blocking.index()]
1974                .active
1975                .high_water
1976                >= 1
1977        );
1978    }
1979
1980    #[test]
1981    fn blocking_executor_supports_bounded_synchronous_callers() {
1982        let runtime = SidecarRuntime::build(RuntimeConfig {
1983            worker_threads: 1,
1984            blocking_worker_threads: 1,
1985            max_queued_blocking_jobs: 1,
1986            max_blocking_job_bytes: 32,
1987            ..RuntimeConfig::default()
1988        })
1989        .expect("build runtime");
1990        let blocking = runtime.context().blocking().clone();
1991        let value = blocking
1992            .run_sync(4, Duration::from_secs(1), || 42usize)
1993            .expect("synchronous blocking job result");
1994        assert_eq!(value, 42);
1995        assert_eq!(blocking.reserved_bytes(), 0);
1996    }
1997
1998    #[test]
1999    fn task_supervisor_reports_every_terminal_reason_and_reconciles() {
2000        let runtime = SidecarRuntime::build(RuntimeConfig {
2001            worker_threads: 1,
2002            blocking_worker_threads: 1,
2003            max_queued_blocking_jobs: 1,
2004            ..RuntimeConfig::default()
2005        })
2006        .expect("build runtime");
2007        let context = runtime.context();
2008
2009        runtime.block_on(async {
2010            context
2011                .spawn(TaskClass::Runtime, async {})
2012                .expect("completed task admission")
2013                .await
2014                .expect("completed task join");
2015
2016            let failed = context
2017                .spawn_result(TaskClass::Runtime, async { Err::<(), _>("failed") })
2018                .expect("failed task admission")
2019                .await
2020                .expect("failed task join");
2021            assert_eq!(failed, Err("failed"));
2022
2023            let panicked = context
2024                .spawn(TaskClass::Runtime, async { panic!("task panic fixture") })
2025                .expect("panicked task admission")
2026                .await;
2027            assert!(panicked.expect_err("task must panic").is_panic());
2028
2029            let cancelled = context
2030                .spawn(TaskClass::Runtime, std::future::pending::<()>())
2031                .expect("cancelled task admission");
2032            cancelled.abort();
2033            assert!(cancelled
2034                .await
2035                .expect_err("task must cancel")
2036                .is_cancelled());
2037        });
2038
2039        let snapshot = context.tasks().snapshot(TaskClass::Runtime);
2040        assert_eq!(snapshot.active, 0);
2041        assert_eq!(snapshot.completed, 1);
2042        assert_eq!(snapshot.failed, 1);
2043        assert_eq!(snapshot.panicked, 1);
2044        assert_eq!(snapshot.cancelled, 1);
2045        assert_eq!(context.resources().usage(ResourceClass::Tasks).used, 0);
2046        let terminal_failure = context
2047            .terminal_failure()
2048            .expect("failed task must latch owner failure");
2049        assert_eq!(terminal_failure.reason, TaskTerminalReason::Failed);
2050        assert_eq!(terminal_failure.owner, TaskOwner::Process);
2051        let metric = context.metrics().snapshot().task(TaskClass::Runtime);
2052        assert_eq!(metric.active, 0);
2053        assert_eq!(metric.terminal_count(TaskTerminalReason::Completed), 1);
2054        assert_eq!(metric.terminal_count(TaskTerminalReason::Failed), 1);
2055        assert_eq!(metric.terminal_count(TaskTerminalReason::Panicked), 1);
2056        assert_eq!(metric.terminal_count(TaskTerminalReason::Cancelled), 1);
2057    }
2058
2059    #[test]
2060    fn long_task_poll_records_watchdog_once_without_dynamic_labels() {
2061        let runtime = SidecarRuntime::build(RuntimeConfig {
2062            worker_threads: 1,
2063            blocking_worker_threads: 1,
2064            max_queued_blocking_jobs: 1,
2065            task_poll_watchdog_ms: 1,
2066            ..RuntimeConfig::default()
2067        })
2068        .expect("build runtime");
2069        let context = runtime.context();
2070        runtime.block_on(async {
2071            context
2072                .spawn(TaskClass::Runtime, async {
2073                    std::thread::sleep(Duration::from_millis(5));
2074                })
2075                .expect("task admission")
2076                .await
2077                .expect("task join");
2078        });
2079        let snapshot = context.metrics().snapshot();
2080        let watchdog = snapshot.watchdogs[WatchdogMetric::LongTaskPoll.index()];
2081        assert_eq!(watchdog.events, 1);
2082        assert!(watchdog.max_stall_micros >= 1_000);
2083        assert_eq!(
2084            snapshot.stderr_fallbacks[TelemetrySeverity::Warning.index()],
2085            1
2086        );
2087    }
2088
2089    #[test]
2090    fn vm_scoped_context_shares_workers_and_charges_both_ledgers() {
2091        let runtime = SidecarRuntime::build(RuntimeConfig {
2092            worker_threads: 1,
2093            blocking_worker_threads: 1,
2094            max_queued_blocking_jobs: 1,
2095            max_blocking_job_bytes: 32,
2096            ..RuntimeConfig::default()
2097        })
2098        .expect("build runtime");
2099        let process = Arc::clone(runtime.context().resources());
2100        let vm_ledger = Arc::new(ResourceLedger::child(
2101            "vm=1 generation=1",
2102            [
2103                (
2104                    ResourceClass::Tasks,
2105                    ResourceLimit::new(1, "limits.reactor.maxTasks"),
2106                ),
2107                (
2108                    ResourceClass::ExecutorSlots,
2109                    ResourceLimit::new(1, "limits.blocking.maxJobs"),
2110                ),
2111                (
2112                    ResourceClass::ExecutorBytes,
2113                    ResourceLimit::new(8, "limits.blocking.maxQueuedBytes"),
2114                ),
2115            ],
2116            Arc::clone(&process),
2117        ));
2118        let scoped = runtime.context().scoped(Arc::clone(&vm_ledger));
2119        assert_eq!(scoped.blocking().worker_count(), 1);
2120
2121        let value = runtime
2122            .block_on(scoped.blocking().run(8, || 42usize))
2123            .expect("VM-scoped blocking job");
2124        assert_eq!(value, 42);
2125        assert!(vm_ledger.is_zero());
2126        assert_eq!(
2127            process.usage(ResourceClass::ExecutorBytes).used,
2128            0,
2129            "child release must reconcile the process parent"
2130        );
2131
2132        runtime.block_on(async {
2133            scoped
2134                .spawn(TaskClass::Vm, async {})
2135                .expect("VM task admission")
2136                .await
2137                .expect("VM task join");
2138        });
2139        assert!(vm_ledger.is_zero());
2140    }
2141
2142    fn assert_vm_generation_churn_reconciles(generation_count: u64, logical_vm_count: u64) {
2143        use crate::capability::{CapabilityBackend, CapabilityKind, CapabilityRegistry};
2144
2145        assert!(generation_count > 0);
2146        assert!(logical_vm_count > 0);
2147
2148        let runtime = SidecarRuntime::build(RuntimeConfig {
2149            worker_threads: 2,
2150            blocking_worker_threads: 2,
2151            max_queued_blocking_jobs: 4,
2152            max_blocking_jobs: 8,
2153            max_blocking_job_bytes: 1024,
2154            ..RuntimeConfig::default()
2155        })
2156        .expect("build runtime");
2157        let process = Arc::clone(runtime.context().resources());
2158
2159        runtime.block_on(async {
2160            for generation in 1..=generation_count {
2161                let vm_index = (generation - 1) % logical_vm_count;
2162                let resources = Arc::new(ResourceLedger::child(
2163                    format!("vm=churn-{vm_index} generation={generation}"),
2164                    [
2165                        (
2166                            ResourceClass::Capabilities,
2167                            ResourceLimit::new(2, "limits.reactor.maxCapabilities"),
2168                        ),
2169                        (
2170                            ResourceClass::ReadyHandles,
2171                            ResourceLimit::new(2, "limits.reactor.maxReadyHandles"),
2172                        ),
2173                        (
2174                            ResourceClass::Sockets,
2175                            ResourceLimit::new(2, "limits.resources.maxSockets"),
2176                        ),
2177                        (
2178                            ResourceClass::Connections,
2179                            ResourceLimit::new(2, "limits.resources.maxConnections"),
2180                        ),
2181                        (
2182                            ResourceClass::Tasks,
2183                            ResourceLimit::new(2, "limits.reactor.maxTasks"),
2184                        ),
2185                        (
2186                            ResourceClass::ExecutorSlots,
2187                            ResourceLimit::new(4, "limits.reactor.maxBlockingJobs"),
2188                        ),
2189                        (
2190                            ResourceClass::ExecutorBytes,
2191                            ResourceLimit::new(64, "limits.reactor.maxBlockingBytes"),
2192                        ),
2193                    ],
2194                    Arc::clone(&process),
2195                ));
2196                let context = runtime
2197                    .context()
2198                    .scoped_for_vm(Arc::clone(&resources), generation);
2199                let capabilities = CapabilityRegistry::new(generation, Arc::clone(&resources));
2200                let lease = capabilities
2201                    .reserve(CapabilityKind::TcpSocket)
2202                    .expect("reserve churn socket before allocation")
2203                    .commit(CapabilityBackend::Kernel {
2204                        socket_id: generation,
2205                    })
2206                    .expect("commit churn socket");
2207
2208                let task = context
2209                    .spawn(TaskClass::Socket, async { tokio::task::yield_now().await })
2210                    .expect("admit churn task");
2211                let blocking = context
2212                    .blocking()
2213                    .run(16, move || generation)
2214                    .await
2215                    .expect("fixed blocking worker result");
2216                assert_eq!(blocking, generation);
2217                task.await.expect("churn task join");
2218
2219                let capability_id = lease.id();
2220                let turn = context
2221                    .fairness()
2222                    .acquire(generation, capability_id, FairBudget::new(1, 64))
2223                    .await
2224                    .expect("churn fairness turn");
2225                turn.complete(FairBudget::new(1, 16), false)
2226                    .expect("complete churn fairness turn");
2227                drop(lease);
2228                capabilities
2229                    .close_admission()
2230                    .expect("close churn capability admission");
2231                context.close_admission();
2232                context.tasks().wait_empty().await;
2233                capabilities.wait_empty().await;
2234                context
2235                    .fairness()
2236                    .retire_capability(generation, capability_id)
2237                    .expect("retire churn capability fairness state");
2238                context
2239                    .fairness()
2240                    .retire_vm(generation)
2241                    .expect("retire churn VM fairness state");
2242                assert!(
2243                    resources.is_zero(),
2244                    "generation {generation} leaked accounting"
2245                );
2246                assert!(resources.integrity_ok());
2247            }
2248        });
2249
2250        assert!(process.is_zero(), "VM churn drifted process accounting");
2251        assert!(process.integrity_ok());
2252        assert_eq!(runtime.context().tasks().active_total(), 0);
2253    }
2254
2255    #[test]
2256    fn vm_generation_churn_reconciles_tasks_capabilities_fairness_and_bytes() {
2257        // Fast default-CI safeguard: enough generations to cross logical VM
2258        // identities repeatedly while keeping this test deterministic.
2259        assert_vm_generation_churn_reconciles(256, 8);
2260    }
2261
2262    #[test]
2263    #[ignore = "expensive: multi-VM runtime accounting soak; run explicitly with --ignored"]
2264    fn multi_vm_generation_soak_has_no_accounting_or_scheduler_drift() {
2265        // The same invariant as the default regression, at a scale intended to
2266        // expose cumulative reservation, tombstone, and task-census drift.
2267        assert_vm_generation_churn_reconciles(50_000, 64);
2268    }
2269
2270    #[test]
2271    fn closing_vm_context_rejects_stale_task_and_blocking_clones() {
2272        let runtime = SidecarRuntime::build(RuntimeConfig {
2273            worker_threads: 1,
2274            blocking_worker_threads: 1,
2275            max_queued_blocking_jobs: 1,
2276            ..RuntimeConfig::default()
2277        })
2278        .expect("build runtime");
2279        let vm_ledger = Arc::new(ResourceLedger::child(
2280            "vm-generation-77",
2281            [
2282                (
2283                    ResourceClass::Tasks,
2284                    ResourceLimit::new(4, "limits.reactor.maxTasks"),
2285                ),
2286                (
2287                    ResourceClass::ExecutorSlots,
2288                    ResourceLimit::new(4, "limits.reactor.maxBlockingJobs"),
2289                ),
2290                (
2291                    ResourceClass::ExecutorBytes,
2292                    ResourceLimit::new(64, "limits.reactor.maxBlockingBytes"),
2293                ),
2294            ],
2295            Arc::clone(runtime.context().resources()),
2296        ));
2297        let scoped = runtime.context().scoped_for_vm(Arc::clone(&vm_ledger), 77);
2298        let stale = scoped.clone();
2299        scoped.close_admission();
2300
2301        let task_error = stale
2302            .spawn(TaskClass::Vm, async {})
2303            .expect_err("closed VM must reject stale task clone");
2304        assert!(matches!(task_error, TaskSpawnError::AdmissionClosed { .. }));
2305        let blocking_error = stale
2306            .blocking()
2307            .submit(1, || {})
2308            .expect_err("closed VM must reject stale blocking clone");
2309        assert_eq!(blocking_error, BlockingJobError::ShuttingDown);
2310        assert!(vm_ledger.is_zero());
2311    }
2312
2313    #[test]
2314    fn close_admission_linearizes_task_and_blocking_rejection() {
2315        let runtime = SidecarRuntime::build(RuntimeConfig {
2316            worker_threads: 1,
2317            blocking_worker_threads: 1,
2318            max_queued_blocking_jobs: 1,
2319            ..RuntimeConfig::default()
2320        })
2321        .expect("build runtime");
2322        let vm_ledger = Arc::new(ResourceLedger::child(
2323            "vm-generation-88",
2324            [
2325                (
2326                    ResourceClass::Tasks,
2327                    ResourceLimit::new(2, "limits.reactor.maxTasks"),
2328                ),
2329                (
2330                    ResourceClass::ExecutorSlots,
2331                    ResourceLimit::new(2, "limits.reactor.maxBlockingJobs"),
2332                ),
2333                (
2334                    ResourceClass::ExecutorBytes,
2335                    ResourceLimit::new(8, "limits.reactor.maxBlockingBytes"),
2336                ),
2337            ],
2338            Arc::clone(runtime.context().resources()),
2339        ));
2340        let scoped = runtime.context().scoped_for_vm(Arc::clone(&vm_ledger), 88);
2341
2342        // Blocking admission and task admission share this exact gate. Holding
2343        // it models an admission operation at its linearization point.
2344        let gate = Arc::clone(&scoped.blocking.admission_gate);
2345        let held = gate.lock().expect("admission gate");
2346        let closing = scoped.clone();
2347        let (started_tx, started_rx) = mpsc::sync_channel(1);
2348        let (closed_tx, closed_rx) = mpsc::sync_channel(1);
2349        let close_thread = thread::spawn(move || {
2350            started_tx.send(()).expect("publish close start");
2351            closing.close_admission();
2352            closed_tx.send(()).expect("publish close completion");
2353        });
2354        started_rx.recv().expect("close started");
2355        assert_eq!(
2356            closed_rx.recv_timeout(Duration::from_millis(20)),
2357            Err(mpsc::RecvTimeoutError::Timeout),
2358            "close must wait for the shared admission linearization gate"
2359        );
2360        drop(held);
2361        closed_rx
2362            .recv_timeout(Duration::from_secs(1))
2363            .expect("close completed after admission released");
2364        close_thread.join().expect("close thread");
2365
2366        let task_error = scoped
2367            .spawn(TaskClass::Vm, async {})
2368            .expect_err("task admission after linearized close must fail");
2369        assert!(matches!(task_error, TaskSpawnError::AdmissionClosed { .. }));
2370        let blocking_error = scoped
2371            .blocking()
2372            .submit(1, || {})
2373            .expect_err("blocking admission after linearized close must fail");
2374        assert_eq!(blocking_error, BlockingJobError::ShuttingDown);
2375        assert!(vm_ledger.is_zero());
2376    }
2377
2378    #[test]
2379    fn closing_vm_context_wakes_admitted_readiness_waiters() {
2380        let runtime = SidecarRuntime::build(RuntimeConfig {
2381            worker_threads: 1,
2382            ..RuntimeConfig::default()
2383        })
2384        .expect("build runtime");
2385        let resources = Arc::new(ResourceLedger::child(
2386            "vm-generation-88",
2387            [(
2388                ResourceClass::Tasks,
2389                ResourceLimit::new(2, "limits.reactor.maxTasks"),
2390            )],
2391            Arc::clone(runtime.context().resources()),
2392        ));
2393        let scoped = runtime.context().scoped_for_vm(resources, 88);
2394        let waiter_context = scoped.clone();
2395        let (started_tx, started_rx) = std::sync::mpsc::sync_channel(1);
2396        let waiter = scoped
2397            .spawn(TaskClass::Socket, async move {
2398                started_tx.send(()).expect("signal waiter admission");
2399                waiter_context.admission_closed().await;
2400            })
2401            .expect("spawn readiness waiter");
2402
2403        started_rx
2404            .recv_timeout(Duration::from_secs(1))
2405            .expect("waiter started");
2406        scoped.close_admission();
2407        runtime.context().handle().block_on(async {
2408            tokio::time::timeout(Duration::from_secs(1), waiter)
2409                .await
2410                .expect("close wakes waiter before teardown deadline")
2411                .expect("waiter joins")
2412        });
2413        assert_eq!(scoped.tasks().active_scoped(), 0);
2414    }
2415}