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