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