1#[cfg(not(test))]
4use std::collections::BTreeMap;
5use std::collections::{HashMap, HashSet};
6use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
7use std::sync::{Arc, Condvar, Mutex, OnceLock};
8use std::thread;
9use std::time::{Duration, Instant};
10
11#[cfg(not(test))]
12use agentos_bridge::queue_tracker::warn_limit_exhausted;
13use agentos_bridge::queue_tracker::{register_queue, QueueGauge, TrackedLimit};
14use agentos_bridge::{bridge_contract, BridgeCallConvention};
15use agentos_runtime::accounting::{Reservation, ResourceClass, ResourceLedger};
16use agentos_runtime::metrics::{ExecutorMetricClass, RuntimeMetrics};
17use agentos_runtime::readiness::{
18 ReadyAcknowledgement, ReadyBatch as RuntimeReadyBatch, ReadyFlags, ReadyObservation, ReadyWake,
19 SessionReadyBroker as RuntimeSessionReadyBroker,
20};
21use agentos_runtime::RuntimeContext;
22use crossbeam_channel::{Receiver, Select, Sender};
23
24use crate::execution;
25#[cfg(test)]
26use crate::host_call::BridgeCallRegistry;
27#[cfg(not(test))]
28use crate::host_call::{BridgeCallContext, ChannelRuntimeEventSender};
29use crate::host_call::{CallIdRouter, SharedCallIdCounter};
30use crate::ipc::ExecutionError;
31#[cfg(not(test))]
32use crate::ipc_binary::{ExecutionErrorBin, ExecutionLimit};
33use crate::runtime_protocol::{
34 BridgeResponse, RuntimeEvent, SessionMessage, StreamEvent, WarmSessionHint,
35};
36use crate::snapshot::{snapshot_cache_key, SnapshotCache, SnapshotCacheKey};
37#[cfg(not(test))]
38use crate::{bridge, isolate, snapshot};
39
40pub enum SessionCommand {
42 Shutdown,
44 Message(SessionMessage),
46 SetModuleReader(Box<dyn crate::execution::GuestModuleReader>),
51 ReadyBatch(RuntimeReadyBatch),
54}
55
56#[derive(Debug)]
57struct SessionReadyWakeState {
58 runtime_wake_rx: tokio::sync::mpsc::Receiver<ReadyWake>,
59}
60
61#[derive(Debug)]
64struct SessionReadiness {
65 generation: u64,
66 max_batch_handles: usize,
67 broker: RuntimeSessionReadyBroker,
68 wakes: Mutex<SessionReadyWakeState>,
69 executor_wake_tx: Sender<ReadyWake>,
70}
71
72impl SessionReadiness {
73 fn new(
74 generation: u64,
75 runtime: &RuntimeContext,
76 max_batch_handles: usize,
77 ) -> Result<(Arc<Self>, Receiver<ReadyWake>), String> {
78 if max_batch_handles == 0 {
79 return Err(String::from(
80 "ERR_AGENTOS_READY_BATCH_LIMIT: limits.reactor.workQuantum must be greater than zero",
81 ));
82 }
83 let (broker, runtime_wake_rx) = RuntimeSessionReadyBroker::new_with_resources(
84 generation,
85 Arc::clone(runtime.resources()),
86 runtime.metrics().clone(),
87 )
88 .map_err(|error| error.to_string())?;
89 let (executor_wake_tx, executor_wake_rx) = crossbeam_channel::bounded(1);
90 Ok((
91 Arc::new(Self {
92 generation,
93 max_batch_handles,
94 broker,
95 wakes: Mutex::new(SessionReadyWakeState { runtime_wake_rx }),
96 executor_wake_tx,
97 }),
98 executor_wake_rx,
99 ))
100 }
101
102 fn disabled(generation: u64) -> Result<(Arc<Self>, Receiver<ReadyWake>), String> {
106 let (broker, runtime_wake_rx) =
107 RuntimeSessionReadyBroker::new(generation, 1).map_err(|error| error.to_string())?;
108 let (executor_wake_tx, executor_wake_rx) = crossbeam_channel::bounded(1);
109 Ok((
110 Arc::new(Self {
111 generation,
112 max_batch_handles: 1,
113 broker,
114 wakes: Mutex::new(SessionReadyWakeState { runtime_wake_rx }),
115 executor_wake_tx,
116 }),
117 executor_wake_rx,
118 ))
119 }
120
121 fn publish(
122 &self,
123 capability_id: u64,
124 capability_generation: u64,
125 flags: ReadyFlags,
126 ) -> Result<(), String> {
127 self.broker
128 .mark_ready(self.generation, capability_id, capability_generation, flags)
129 .map_err(|error| error.to_string())?;
130 let mut state = self.wakes.lock().map_err(|_| {
131 String::from("ERR_AGENTOS_READY_STATE_POISONED: session readiness lock poisoned")
132 })?;
133 self.forward_runtime_wake_locked(&mut state)
134 }
135
136 fn publish_signal(&self, signal: i32) -> Result<(), String> {
137 self.broker
138 .mark_signal_ready(self.generation, signal)
139 .map_err(|error| error.to_string())?;
140 let mut state = self.wakes.lock().map_err(|_| {
141 String::from("ERR_AGENTOS_READY_STATE_POISONED: session readiness lock poisoned")
142 })?;
143 self.forward_runtime_wake_locked(&mut state)
144 }
145
146 fn remove(&self, capability_id: u64, capability_generation: u64) -> Result<(), String> {
147 self.broker
148 .remove_capability(self.generation, capability_id, capability_generation)
149 .map_err(|error| error.to_string())
150 }
151
152 fn set_application_read_interest(
153 &self,
154 capability_id: u64,
155 capability_generation: u64,
156 enabled: bool,
157 ) -> Result<(), String> {
158 self.broker
159 .set_application_read_interest(
160 self.generation,
161 capability_id,
162 capability_generation,
163 enabled,
164 )
165 .map_err(|error| error.to_string())
166 }
167
168 fn publish_timer(&self, timer_id: u64) -> Result<(), String> {
169 self.broker
170 .mark_timer_ready(self.generation, timer_id)
171 .map_err(|error| error.to_string())?;
172 let mut state = self.wakes.lock().map_err(|_| {
173 String::from("ERR_AGENTOS_READY_STATE_POISONED: session readiness lock poisoned")
174 })?;
175 self.forward_runtime_wake_locked(&mut state)
176 }
177
178 fn forward_runtime_wake_locked(&self, state: &mut SessionReadyWakeState) -> Result<(), String> {
179 let wake = match state.runtime_wake_rx.try_recv() {
180 Ok(wake) => wake,
181 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => return Ok(()),
182 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
183 return Err(String::from(
184 "ERR_AGENTOS_READY_WAKE_DISCONNECTED: shared readiness wake source disconnected",
185 ));
186 }
187 };
188 match self.executor_wake_tx.try_send(wake) {
189 Ok(()) => Ok(()),
190 Err(crossbeam_channel::TrySendError::Full(_)) => Err(String::from(
191 "ERR_AGENTOS_READY_WAKE_INVARIANT: executor readiness lane was full for a shared wake",
192 )),
193 Err(crossbeam_channel::TrySendError::Disconnected(_)) => Err(String::from(
194 "ERR_AGENTOS_READY_WAKE_DISCONNECTED: executor readiness consumer disconnected",
195 )),
196 }
197 }
198
199 fn take_batch(&self, wake: ReadyWake) -> Result<RuntimeReadyBatch, String> {
200 self.broker
201 .ready_batch(wake.generation, wake.epoch, self.max_batch_handles)
202 .map_err(|error| error.to_string())
203 }
204
205 fn drain_signals(&self, batch: &RuntimeReadyBatch) -> Result<Vec<i32>, String> {
206 self.broker
207 .drain_signals(batch.generation, batch.epoch, self.max_batch_handles)
208 .map_err(|error| error.to_string())
209 }
210
211 fn drain_timers(&self, batch: &RuntimeReadyBatch) -> Result<Vec<u64>, String> {
212 self.broker
213 .drain_timers(batch.generation, batch.epoch, self.max_batch_handles)
214 .map_err(|error| error.to_string())
215 }
216
217 fn complete_batch(
218 &self,
219 batch: &RuntimeReadyBatch,
220 delivered: &[ReadyObservation],
221 ) -> Result<(), String> {
222 let acknowledgements = delivered
223 .iter()
224 .map(|entry| ReadyAcknowledgement {
225 capability_id: entry.capability_id,
226 capability_generation: entry.capability_generation,
227 observed_revision: entry.revision,
228 clear: entry.flags,
229 })
230 .collect::<Vec<_>>();
231 self.broker
232 .complete_wake(batch.generation, batch.epoch, &acknowledgements)
233 .map_err(|error| error.to_string())?;
234 let mut state = self.wakes.lock().map_err(|_| {
235 String::from("ERR_AGENTOS_READY_STATE_POISONED: session readiness lock poisoned")
236 })?;
237 self.forward_runtime_wake_locked(&mut state)
238 }
239}
240
241#[derive(Default)]
242struct SessionPauseState {
243 paused: bool,
244 shutdown: bool,
245}
246
247#[derive(Default)]
248#[doc(hidden)]
249pub struct SessionPauseControl {
250 state: Mutex<SessionPauseState>,
251 resumed: Condvar,
252}
253
254impl SessionPauseControl {
255 pub(crate) fn pause(&self) {
256 self.state
257 .lock()
258 .expect("session pause lock poisoned")
259 .paused = true;
260 }
261
262 pub(crate) fn resume(&self) {
263 let mut state = self.state.lock().expect("session pause lock poisoned");
264 state.paused = false;
265 self.resumed.notify_all();
266 }
267
268 fn shutdown(&self) {
269 let mut state = self.state.lock().expect("session pause lock poisoned");
270 state.shutdown = true;
271 state.paused = false;
272 self.resumed.notify_all();
273 }
274
275 pub(crate) fn wait_while_paused(&self) {
276 let mut state = self.state.lock().expect("session pause lock poisoned");
277 while state.paused && !state.shutdown {
278 state = self
279 .resumed
280 .wait(state)
281 .expect("session pause lock poisoned while waiting");
282 }
283 }
284}
285
286#[cfg(not(test))]
287extern "C" fn pause_isolate_interrupt(_isolate: &mut v8::Isolate, data: *mut std::ffi::c_void) {
288 let control = unsafe { Arc::from_raw(data.cast::<SessionPauseControl>()) };
291 control.wait_while_paused();
292}
293
294#[cfg(not(test))]
295type SharedIsolateHandle = Arc<Mutex<Option<v8::IsolateHandle>>>;
296#[cfg(test)]
297type SharedIsolateHandle = Arc<Mutex<Option<()>>>;
298
299#[derive(Clone)]
304pub enum RuntimeEventSender {
305 Channel(crossbeam_channel::Sender<RuntimeEventEnvelope>),
306 Closed,
307 Direct {
308 generation: u64,
309 sender: RuntimeEventOutputSender,
310 },
311}
312
313impl RuntimeEventSender {
314 pub fn closed() -> Self {
315 Self::Closed
316 }
317
318 pub fn direct(generation: u64, sender: RuntimeEventOutputSender) -> Self {
319 Self::Direct { generation, sender }
320 }
321
322 pub fn send(&self, envelope: RuntimeEventEnvelope) -> Result<(), String> {
323 match self {
324 Self::Channel(sender) => sender.try_send(envelope).map_err(|error| match error {
325 crossbeam_channel::TrySendError::Full(_) => String::from(
326 "ERR_AGENTOS_V8_OUTPUT_LIMIT: runtime output lane is full; raise runtime.resources.maxAsyncCompletions",
327 ),
328 crossbeam_channel::TrySendError::Disconnected(_) => String::from(
329 "ERR_AGENTOS_V8_OUTPUT_DISCONNECTED: runtime output consumer disconnected",
330 ),
331 }),
332 Self::Closed => Err(String::from(
333 "ERR_AGENTOS_V8_OUTPUT_UNREGISTERED: session has no registered output lane",
334 )),
335 Self::Direct { generation, sender } => {
336 if envelope.output_generation != Some(*generation) {
337 return Err(format!(
338 "ERR_AGENTOS_STALE_V8_OUTPUT: event generation {:?} does not match registered generation {generation}",
339 envelope.output_generation
340 ));
341 }
342 sender.try_send(envelope.event)
343 }
344 }
345 }
346}
347
348impl From<crossbeam_channel::Sender<RuntimeEventEnvelope>> for RuntimeEventSender {
349 fn from(sender: crossbeam_channel::Sender<RuntimeEventEnvelope>) -> Self {
350 Self::Channel(sender)
351 }
352}
353
354#[derive(Debug, Clone, PartialEq)]
355pub struct RuntimeEventEnvelope {
356 pub output_generation: Option<u64>,
357 pub event: RuntimeEvent,
358}
359
360#[derive(Clone)]
361pub struct RuntimeEventOutputSender {
362 inner: flume::Sender<QueuedRuntimeEvent>,
363 resources: Arc<ResourceLedger>,
364 gauge: Arc<QueueGauge>,
365}
366
367pub struct RuntimeEventOutputReceiver {
368 inner: flume::Receiver<QueuedRuntimeEvent>,
369 gauge: Arc<QueueGauge>,
370}
371
372struct QueuedRuntimeEvent {
373 event: RuntimeEvent,
374 _reservation: Reservation,
375}
376
377pub fn runtime_event_output_channel(
378 capacity: usize,
379 resources: Arc<ResourceLedger>,
380) -> (RuntimeEventOutputSender, RuntimeEventOutputReceiver) {
381 let (sender, receiver) = flume::bounded(capacity);
382 let gauge = register_queue(TrackedLimit::V8SessionFrames, capacity);
383 (
384 RuntimeEventOutputSender {
385 inner: sender,
386 resources,
387 gauge: Arc::clone(&gauge),
388 },
389 RuntimeEventOutputReceiver {
390 inner: receiver,
391 gauge,
392 },
393 )
394}
395
396impl RuntimeEventOutputSender {
397 #[cfg(test)]
398 pub(crate) fn capacity(&self) -> Option<usize> {
399 self.inner.capacity()
400 }
401
402 pub fn send(&self, event: RuntimeEvent) -> Result<(), String> {
403 let reservation = self
404 .resources
405 .reserve(ResourceClass::AsyncCompletions, 1)
406 .map_err(|error| error.to_string())?;
407 let result = self
408 .inner
409 .send(QueuedRuntimeEvent {
410 event,
411 _reservation: reservation,
412 })
413 .map_err(|_| {
414 String::from(
415 "ERR_AGENTOS_V8_OUTPUT_DISCONNECTED: session output consumer disconnected",
416 )
417 });
418 self.gauge.observe_depth(self.inner.len());
419 result
420 }
421
422 pub fn try_send(&self, event: RuntimeEvent) -> Result<(), String> {
423 let reservation = self
424 .resources
425 .reserve(ResourceClass::AsyncCompletions, 1)
426 .map_err(|error| error.to_string())?;
427 let result = self
428 .inner
429 .try_send(QueuedRuntimeEvent {
430 event,
431 _reservation: reservation,
432 })
433 .map_err(|error| match error {
434 flume::TrySendError::Full(_) => String::from(
435 "ERR_AGENTOS_V8_OUTPUT_LIMIT: session output lane is full; raise limits.reactor.maxAsyncCompletions",
436 ),
437 flume::TrySendError::Disconnected(_) => String::from(
438 "ERR_AGENTOS_V8_OUTPUT_DISCONNECTED: session output consumer disconnected",
439 ),
440 });
441 self.gauge.observe_depth(self.inner.len());
442 result
443 }
444}
445
446impl RuntimeEventOutputReceiver {
447 pub fn recv(&self) -> Result<RuntimeEvent, flume::RecvError> {
448 let result = self.inner.recv().map(|queued| queued.event);
449 self.gauge.observe_depth(self.inner.len());
450 result
451 }
452
453 pub fn recv_timeout(&self, timeout: Duration) -> Result<RuntimeEvent, flume::RecvTimeoutError> {
454 let result = self.inner.recv_timeout(timeout).map(|queued| queued.event);
455 self.gauge.observe_depth(self.inner.len());
456 result
457 }
458
459 pub fn try_recv(&self) -> Result<RuntimeEvent, flume::TryRecvError> {
460 let result = self.inner.try_recv().map(|queued| queued.event);
461 self.gauge.observe_depth(self.inner.len());
462 result
463 }
464
465 pub async fn recv_async(&self) -> Result<RuntimeEvent, flume::RecvError> {
466 let result = self.inner.recv_async().await.map(|queued| queued.event);
467 self.gauge.observe_depth(self.inner.len());
468 result
469 }
470}
471
472impl Drop for RuntimeEventOutputReceiver {
473 fn drop(&mut self) {
474 while self.inner.try_recv().is_ok() {}
475 self.gauge.observe_depth(self.inner.len());
476 }
477}
478
479const LATE_TERMINATE_EXECUTION_ERROR_CODE: &str = "ERR_LATE_TERMINATE_EXECUTION";
480const LATE_STREAM_EVENT_ERROR_CODE: &str = "ERR_LATE_STREAM_EVENT";
481const LATE_BRIDGE_RESPONSE_ERROR_CODE: &str = "ERR_LATE_BRIDGE_RESPONSE";
482#[derive(Clone, Debug, Eq, Hash, PartialEq)]
483struct WarmPoolKey {
484 snapshot_key_digest: SnapshotCacheKey,
485 heap_limit_mb: u32,
486}
487
488struct ParkedWorker {
489 assignment_tx: Sender<SessionAssignment>,
490 join_handle: thread::JoinHandle<()>,
491}
492
493#[derive(Default)]
494struct WarmWorkerPoolState {
495 workers: HashMap<WarmPoolKey, Vec<ParkedWorker>>,
496 refilling: HashSet<WarmPoolKey>,
497 reserved_workers: usize,
498}
499
500#[derive(Default)]
501struct WarmWorkerPool {
502 state: Mutex<WarmWorkerPoolState>,
503}
504
505struct SessionAssignment {
506 heap_limit_mb: Option<u32>,
507 cpu_time_limit_ms: Option<u32>,
508 wall_clock_limit_ms: Option<u32>,
509 rx: Receiver<SessionCommand>,
510 shutdown_rx: Receiver<()>,
511 ready_rx: Receiver<ReadyWake>,
512 ready_broker: Arc<SessionReadiness>,
513 slot_permit: SessionSlotPermit,
514 event_tx: RuntimeEventSender,
515 call_id_router: CallIdRouter,
516 shared_call_id: SharedCallIdCounter,
517 snapshot_cache: Arc<SnapshotCache>,
518 isolate_handle: SharedIsolateHandle,
519 execution_abort: SharedExecutionAbort,
520 execution_active: Arc<AtomicBool>,
521 pause_control: Arc<SessionPauseControl>,
522 session_id: String,
523 output_generation: Option<u64>,
524 runtime: RuntimeContext,
525 bridge_call_timeout: Duration,
526}
527
528#[cfg(not(test))]
529struct PrecreatedIsolate {
530 isolate: Option<v8::OwnedIsolate>,
534 context: Option<v8::Global<v8::Context>>,
535 bridge_code: String,
536 userland_code: String,
537}
538
539#[cfg(not(test))]
540impl Drop for PrecreatedIsolate {
541 fn drop(&mut self) {
542 drop(self.context.take());
543 isolate::drop_isolate(self.isolate.take());
544 }
545}
546
547#[cfg(test)]
548struct PrecreatedIsolate;
549
550#[cfg(not(test))]
551#[derive(Default)]
552struct V8SessionPhaseStats {
553 calls: u64,
554 total_ns: u128,
555 max_ns: u128,
556}
557
558#[cfg(not(test))]
559static V8_SESSION_PHASES: OnceLock<Mutex<BTreeMap<String, V8SessionPhaseStats>>> = OnceLock::new();
560
561#[cfg(not(test))]
562fn v8_session_phases_enabled() -> bool {
563 std::env::var("AGENTOS_V8_SESSION_PHASES").as_deref() == Ok("1")
564}
565
566#[cfg(not(test))]
567fn record_v8_session_phase(stage: &str, elapsed: Duration) {
568 if !v8_session_phases_enabled() {
569 return;
570 }
571 let phases = V8_SESSION_PHASES.get_or_init(|| Mutex::new(BTreeMap::new()));
572 let Ok(mut phases) = phases.lock() else {
573 return;
574 };
575 let stats = phases.entry(stage.to_string()).or_default();
576 stats.calls += 1;
577 let elapsed_ns = elapsed.as_nanos();
578 stats.total_ns += elapsed_ns;
579 stats.max_ns = stats.max_ns.max(elapsed_ns);
580
581 let Some(path) = std::env::var_os("AGENTOS_V8_SESSION_PHASES_FILE") else {
582 return;
583 };
584 let mut output = String::new();
585 for (stage, stats) in phases.iter() {
586 let total_us = stats.total_ns / 1_000;
587 let avg_us = if stats.calls == 0 {
588 0
589 } else {
590 total_us / u128::from(stats.calls)
591 };
592 let max_us = stats.max_ns / 1_000;
593 output.push_str(&format!(
594 "stage={stage} calls={} total_us={total_us} avg_us={avg_us} max_us={max_us}\n",
595 stats.calls
596 ));
597 }
598 let _ = std::fs::write(path, output);
599}
600
601#[cfg(not(test))]
602fn record_warm_worker_hit() {
603 record_v8_session_phase("warm_worker_hit", Duration::ZERO);
604}
605
606#[cfg(test)]
607fn record_warm_worker_hit() {}
608
609#[cfg(not(test))]
610fn record_warm_worker_miss() {
611 record_v8_session_phase("warm_worker_miss", Duration::ZERO);
612}
613
614#[cfg(test)]
615fn record_warm_worker_miss() {}
616
617fn warm_worker_capacity_per_key() -> usize {
618 std::env::var("AGENTOS_V8_WARM_ISOLATES")
619 .ok()
620 .and_then(|value| value.parse::<usize>().ok())
621 .unwrap_or(MAX_PROCESS_WARM_WORKERS)
622 .min(MAX_PROCESS_WARM_WORKERS)
623}
624
625const MAX_PROCESS_WARM_WORKERS: usize = 4;
626
627fn effective_heap_limit_mb(heap_limit_mb: Option<u32>) -> u32 {
628 heap_limit_mb.unwrap_or(crate::isolate::DEFAULT_HEAP_LIMIT_MB)
629}
630
631fn warm_pool_key(
632 bridge_code: &str,
633 userland_code: &str,
634 heap_limit_mb: Option<u32>,
635) -> WarmPoolKey {
636 WarmPoolKey {
637 snapshot_key_digest: snapshot_cache_key(
638 bridge_code,
639 (!userland_code.is_empty()).then_some(userland_code),
640 ),
641 heap_limit_mb: effective_heap_limit_mb(heap_limit_mb),
642 }
643}
644
645fn warm_key_prefix(key: &WarmPoolKey) -> String {
646 key.snapshot_key_digest[..4]
647 .iter()
648 .map(|byte| format!("{byte:02x}"))
649 .collect()
650}
651
652impl WarmWorkerPool {
653 fn claim(&self, key: &WarmPoolKey) -> Option<ParkedWorker> {
654 let mut state = self.state.lock().expect("warm worker pool lock poisoned");
655 state.workers.get_mut(key).and_then(Vec::pop)
656 }
657
658 fn shutdown_handles(&self) -> Vec<thread::JoinHandle<()>> {
659 let mut state = self.state.lock().expect("warm worker pool lock poisoned");
660 state.refilling.clear();
661 state.reserved_workers = 0;
662 state
663 .workers
664 .drain()
665 .flat_map(|(_, workers)| workers)
666 .map(|worker| {
667 drop(worker.assignment_tx);
668 worker.join_handle
669 })
670 .collect()
671 }
672
673 #[allow(clippy::too_many_arguments)]
674 fn ensure_count(
675 self: &Arc<Self>,
676 runtime: RuntimeContext,
677 snapshot_cache: Arc<SnapshotCache>,
678 slot_control: SlotControl,
679 bridge_code: String,
680 userland_code: String,
681 heap_limit_mb: Option<u32>,
682 requested_count: usize,
683 ) {
684 let capacity = warm_worker_capacity_per_key();
685 if capacity == 0 || requested_count == 0 {
686 return;
687 }
688
689 let target_count = requested_count.min(capacity);
690 let key = warm_pool_key(&bridge_code, &userland_code, heap_limit_mb);
691 {
692 let mut state = self.state.lock().expect("warm worker pool lock poisoned");
693 let current = state.workers.get(&key).map_or(0, Vec::len);
694 if current >= target_count || state.refilling.contains(&key) {
695 return;
696 }
697 state.refilling.insert(key.clone());
698 }
699
700 let pool = Arc::clone(self);
701 let spawn_key = key.clone();
702 let requested_bytes = bridge_code.len().saturating_add(userland_code.len());
703 if let Err(error) = runtime.blocking().submit(requested_bytes, move || {
704 pool.refill_until(
705 snapshot_cache,
706 slot_control,
707 spawn_key,
708 bridge_code,
709 userland_code,
710 heap_limit_mb,
711 target_count,
712 );
713 }) {
714 eprintln!("ERR_AGENTOS_V8_WARM_REFILL: bounded executor rejected refill: {error}");
715 self.state
716 .lock()
717 .expect("warm worker pool lock poisoned")
718 .refilling
719 .remove(&key);
720 }
721 }
722
723 #[allow(clippy::too_many_arguments)]
725 fn refill_until(
726 &self,
727 snapshot_cache: Arc<SnapshotCache>,
728 slot_control: SlotControl,
729 key: WarmPoolKey,
730 bridge_code: String,
731 userland_code: String,
732 heap_limit_mb: Option<u32>,
733 target_count: usize,
734 ) {
735 loop {
736 let capacity = warm_worker_capacity_per_key();
737 if capacity == 0 {
738 break;
739 }
740 let desired = target_count.min(capacity);
741 let refill_slot = {
742 let mut state = self.state.lock().expect("warm worker pool lock poisoned");
743 let current = state.workers.get(&key).map_or(0, Vec::len);
744 let total = state.workers.values().map(Vec::len).sum::<usize>();
745 if current >= desired {
746 break;
747 }
748 if total.saturating_add(state.reserved_workers) < MAX_PROCESS_WARM_WORKERS {
749 state.reserved_workers += 1;
750 Some(None)
751 } else {
752 let evict_key = state
753 .workers
754 .iter()
755 .find(|(candidate, workers)| *candidate != &key && !workers.is_empty())
756 .map(|(candidate, _)| candidate.clone());
757 evict_key.and_then(|evict_key| {
758 let workers = state
759 .workers
760 .get_mut(&evict_key)
761 .expect("selected warm worker key exists");
762 let worker = workers.pop();
763 if workers.is_empty() {
764 state.workers.remove(&evict_key);
765 }
766 worker.map(|worker| Some((evict_key, worker)))
767 })
768 }
769 };
770 let Some(evicted) = refill_slot else {
771 break;
772 };
773 if let Some((evicted_key, worker)) = evicted {
774 drop(worker.assignment_tx);
775 let _ = worker.join_handle.join();
776 eprintln!(
777 "agentos-v8-runtime: warm worker evicted key={} heap={}",
778 warm_key_prefix(&evicted_key),
779 evicted_key.heap_limit_mb
780 );
781 continue;
782 }
783
784 let worker = spawn_warm_worker(
785 Arc::clone(&snapshot_cache),
786 Arc::clone(&slot_control),
787 key.clone(),
788 bridge_code.clone(),
789 userland_code.clone(),
790 heap_limit_mb,
791 );
792 let mut state = self.state.lock().expect("warm worker pool lock poisoned");
793 state.reserved_workers = state.reserved_workers.saturating_sub(1);
794 let Some(worker) = worker else {
795 break;
796 };
797
798 let workers = state.workers.entry(key.clone()).or_default();
799 if workers.len() >= desired {
800 drop(worker.assignment_tx);
801 let _ = worker.join_handle.join();
802 break;
803 }
804 workers.push(worker);
805 eprintln!(
806 "agentos-v8-runtime: warm worker refilled key={} heap={} pool_size={}",
807 warm_key_prefix(&key),
808 key.heap_limit_mb,
809 workers.len()
810 );
811 }
812
813 self.state
814 .lock()
815 .expect("warm worker pool lock poisoned")
816 .refilling
817 .remove(&key);
818 }
819}
820
821#[cfg(not(test))]
822fn spawn_warm_worker(
823 snapshot_cache: Arc<SnapshotCache>,
824 slot_control: SlotControl,
825 key: WarmPoolKey,
826 bridge_code: String,
827 userland_code: String,
828 heap_limit_mb: Option<u32>,
829) -> Option<ParkedWorker> {
830 let (assignment_tx, assignment_rx) = crossbeam_channel::bounded::<SessionAssignment>(1);
831 let (ready_tx, ready_rx) = crossbeam_channel::bounded::<Result<(), String>>(1);
832 let worker_bridge_code = bridge_code.clone();
833 let worker_userland_code = userland_code.clone();
834 let join_handle = match thread::Builder::new()
836 .name(String::from("agentos-v8-warm-worker"))
837 .spawn(move || {
838 let precreated = precreate_warm_isolate(
839 snapshot_cache,
840 slot_control,
841 worker_bridge_code,
842 worker_userland_code,
843 heap_limit_mb,
844 );
845 match precreated {
846 Ok(precreated) => {
847 if ready_tx.send(Ok(())).is_err() {
848 eprintln!("INFO_AGENTOS_STALE_WARM_WORKER: warm worker requester disconnected before startup completed");
849 }
850 if let Ok(assignment) = assignment_rx.recv() {
851 session_thread(assignment, Some(precreated));
852 }
853 }
854 Err(error) => {
855 if ready_tx.send(Err(error)).is_err() {
856 eprintln!("INFO_AGENTOS_STALE_WARM_WORKER: warm worker requester disconnected before startup failure was delivered");
857 }
858 }
859 }
860 }) {
861 Ok(handle) => handle,
862 Err(error) => {
863 eprintln!("agentos-v8-runtime: warm worker spawn failed: {error}");
864 return None;
865 }
866 };
867
868 match ready_rx.recv() {
869 Ok(Ok(())) => Some(ParkedWorker {
870 assignment_tx,
871 join_handle,
872 }),
873 Ok(Err(error)) => {
874 eprintln!(
875 "agentos-v8-runtime: warm worker refill failed key={} heap={}: {error}",
876 warm_key_prefix(&key),
877 key.heap_limit_mb
878 );
879 let _ = join_handle.join();
880 None
881 }
882 Err(error) => {
883 eprintln!(
884 "agentos-v8-runtime: warm worker refill failed key={} heap={}: {error}",
885 warm_key_prefix(&key),
886 key.heap_limit_mb
887 );
888 let _ = join_handle.join();
889 None
890 }
891 }
892}
893
894#[cfg(test)]
895fn spawn_warm_worker(
896 _snapshot_cache: Arc<SnapshotCache>,
897 _slot_control: SlotControl,
898 _key: WarmPoolKey,
899 _bridge_code: String,
900 _userland_code: String,
901 _heap_limit_mb: Option<u32>,
902) -> Option<ParkedWorker> {
903 None
904}
905
906#[cfg(not(test))]
907fn precreate_warm_isolate(
908 snapshot_cache: Arc<SnapshotCache>,
909 _slot_control: SlotControl,
910 bridge_code: String,
911 userland_code: String,
912 heap_limit_mb: Option<u32>,
913) -> Result<PrecreatedIsolate, String> {
914 isolate::init_v8_platform();
915 let snapshot_blob = snapshot_cache.get_or_create_with_userland(
916 &bridge_code,
917 (!userland_code.is_empty()).then_some(userland_code.as_str()),
918 )?;
919 let snapshot_blob = (*snapshot_blob).clone();
920 let mut isolate = snapshot::create_isolate_from_snapshot(snapshot_blob, heap_limit_mb);
925 isolate.set_host_import_module_dynamically_callback(execution::dynamic_import_callback);
926 isolate.set_host_initialize_import_meta_object_callback(execution::import_meta_object_callback);
927 let context = isolate::create_context(&mut isolate);
928 Ok(PrecreatedIsolate {
929 isolate: Some(isolate),
930 context: Some(context),
931 bridge_code,
932 userland_code,
933 })
934}
935
936fn normalize_cpu_time_limit_ms(cpu_time_limit_ms: Option<u32>) -> Option<u32> {
942 cpu_time_limit_ms.filter(|budget_ms| *budget_ms > 0)
943}
944
945fn normalize_wall_clock_limit_ms(wall_clock_limit_ms: Option<u32>) -> Option<u32> {
951 wall_clock_limit_ms.filter(|limit_ms| *limit_ms > 0)
952}
953
954fn signal_session_shutdown(sender: &Sender<()>, session_id: &str) {
955 match sender.try_send(()) {
956 Ok(()) => {}
957 Err(crossbeam_channel::TrySendError::Full(())) => eprintln!(
958 "INFO_AGENTOS_VM_SHUTDOWN_COALESCED: session={session_id} already has a pending shutdown signal"
959 ),
960 Err(crossbeam_channel::TrySendError::Disconnected(())) => eprintln!(
961 "INFO_AGENTOS_STALE_VM_SHUTDOWN: session={session_id} executor already disconnected"
962 ),
963 }
964}
965
966pub(crate) fn configured_resource_capacity(
967 runtime: &RuntimeContext,
968 resource: ResourceClass,
969 vm_config_path: &'static str,
970 process_config_path: &'static str,
971) -> Result<usize, String> {
972 let config_path = if runtime.vm_generation().is_some() {
973 vm_config_path
974 } else {
975 process_config_path
976 };
977 match runtime.resources().usage(resource).limit {
978 Some(capacity) if capacity > 0 => Ok(capacity),
979 Some(_) => Err(format!(
980 "ERR_AGENTOS_RUNTIME_CONFIG: {config_path} must be greater than zero"
981 )),
982 None => Err(format!(
983 "ERR_AGENTOS_RUNTIME_CONFIG: {config_path} must configure a bounded {} limit",
984 resource.name()
985 )),
986 }
987}
988
989struct SessionEntry {
991 output_generation: Option<u64>,
993 tx: Sender<SessionCommand>,
995 command_capacity: usize,
997 shutdown_tx: Sender<()>,
1001 join_handle: Option<thread::JoinHandle<()>>,
1003 #[cfg_attr(test, allow(dead_code))]
1005 isolate_handle: SharedIsolateHandle,
1006 execution_abort: SharedExecutionAbort,
1008 execution_active: Arc<AtomicBool>,
1011 pause_control: Arc<SessionPauseControl>,
1012 ready_broker: Arc<SessionReadiness>,
1014 #[cfg(test)]
1015 session_resources: Arc<agentos_runtime::accounting::ResourceLedger>,
1016}
1017
1018pub struct SessionShutdown {
1025 session_id: String,
1026 output_generation: Option<u64>,
1027 join_handle: Option<thread::JoinHandle<()>>,
1028 call_id_router: CallIdRouter,
1029}
1030
1031impl SessionShutdown {
1032 pub fn finish(mut self) {
1033 if let Some(handle) = self.join_handle.take() {
1034 if handle.join().is_err() {
1035 eprintln!(
1036 "ERR_AGENTOS_VM_EXECUTOR_PANIC: session={} generation={:?}",
1037 self.session_id, self.output_generation
1038 );
1039 }
1040 }
1041 self.call_id_router
1042 .cancel_session(&self.session_id, self.output_generation);
1043 }
1044}
1045
1046type SlotControl = Arc<(Mutex<usize>, Condvar)>;
1048
1049struct SessionSlotPermit {
1054 control: SlotControl,
1055 metrics: RuntimeMetrics,
1056}
1057
1058impl SessionSlotPermit {
1059 fn try_acquire(
1060 control: &SlotControl,
1061 maximum: Option<usize>,
1062 metrics: RuntimeMetrics,
1063 ) -> Result<Self, String> {
1064 let (lock, _) = &**control;
1065 let mut active = lock
1066 .lock()
1067 .map_err(|_| String::from("ERR_AGENTOS_VM_EXECUTOR_POISONED: slot lock poisoned"))?;
1068 if maximum.is_some_and(|maximum| *active >= maximum) {
1069 let maximum = maximum.expect("checked as present");
1070 return Err(format!(
1071 "ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 executors reached limit of {maximum}; raise runtime.executor.maxActiveVms"
1072 ));
1073 }
1074 *active += 1;
1075 metrics.observe_executor(ExecutorMetricClass::Vm, *active, 0);
1076 Ok(Self {
1077 control: Arc::clone(control),
1078 metrics,
1079 })
1080 }
1081}
1082
1083impl Drop for SessionSlotPermit {
1084 fn drop(&mut self) {
1085 let (lock, cvar) = &*self.control;
1086 match lock.lock() {
1087 Ok(mut active) if *active > 0 => {
1088 *active -= 1;
1089 self.metrics
1090 .observe_executor(ExecutorMetricClass::Vm, *active, 0);
1091 cvar.notify_all();
1092 }
1093 Ok(_) => eprintln!(
1094 "ERR_AGENTOS_VM_EXECUTOR_ACCOUNTING_UNDERFLOW: executor permit released at zero"
1095 ),
1096 Err(_) => {
1097 eprintln!("ERR_AGENTOS_VM_EXECUTOR_POISONED: executor permit could not be released")
1098 }
1099 }
1100 }
1101}
1102
1103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1104pub(crate) enum ExecutionAbortReason {
1105 Terminated,
1107 #[cfg_attr(test, allow(dead_code))]
1112 WallClockTimedOut,
1113 #[cfg_attr(test, allow(dead_code))]
1115 CpuBudgetExceeded,
1116}
1117
1118struct ExecutionAbortState {
1119 sender: Option<crossbeam_channel::Sender<()>>,
1120 reason: Option<ExecutionAbortReason>,
1121}
1122
1123pub(crate) struct SharedExecutionAbort(Arc<Mutex<Option<ExecutionAbortState>>>);
1124
1125impl Clone for SharedExecutionAbort {
1126 fn clone(&self) -> Self {
1127 Self(Arc::clone(&self.0))
1128 }
1129}
1130
1131pub(crate) fn new_execution_abort() -> SharedExecutionAbort {
1132 SharedExecutionAbort(Arc::new(Mutex::new(None)))
1133}
1134
1135#[cfg_attr(test, allow(dead_code))]
1136pub(crate) struct ActiveExecutionAbort {
1137 shared: SharedExecutionAbort,
1138}
1139
1140#[cfg_attr(test, allow(dead_code))]
1141impl ActiveExecutionAbort {
1142 pub(crate) fn arm(shared: &SharedExecutionAbort) -> (Self, crossbeam_channel::Receiver<()>) {
1143 let (tx, rx) = crossbeam_channel::bounded::<()>(0);
1144 let mut guard = shared.0.lock().unwrap();
1145 if let Some(reason) = guard.as_ref().and_then(|state| state.reason) {
1146 drop(tx);
1151 *guard = Some(ExecutionAbortState {
1152 sender: None,
1153 reason: Some(reason),
1154 });
1155 } else {
1156 *guard = Some(ExecutionAbortState {
1157 sender: Some(tx),
1158 reason: None,
1159 });
1160 }
1161 (
1162 Self {
1163 shared: shared.clone(),
1164 },
1165 rx,
1166 )
1167 }
1168}
1169
1170impl Drop for ActiveExecutionAbort {
1171 fn drop(&mut self) {
1172 *self.shared.0.lock().unwrap() = None;
1173 }
1174}
1175
1176struct ExecutionActivityGuard(Arc<AtomicBool>);
1177
1178impl Drop for ExecutionActivityGuard {
1179 fn drop(&mut self) {
1180 self.0.store(false, Ordering::Release);
1181 }
1182}
1183
1184pub(crate) fn signal_execution_abort(shared: &SharedExecutionAbort, reason: ExecutionAbortReason) {
1185 let mut guard = shared.0.lock().unwrap();
1186 if let Some(state) = guard.as_mut() {
1187 state.reason.get_or_insert(reason);
1188 state.sender.take();
1189 }
1190}
1191
1192fn signal_execution_abort_durable(shared: &SharedExecutionAbort, reason: ExecutionAbortReason) {
1193 let mut guard = shared.0.lock().unwrap();
1194 if let Some(state) = guard.as_mut() {
1195 state.reason.get_or_insert(reason);
1196 state.sender.take();
1197 } else {
1198 *guard = Some(ExecutionAbortState {
1202 sender: None,
1203 reason: Some(reason),
1204 });
1205 }
1206}
1207
1208#[cfg(not(test))]
1209fn execution_abort_reason(shared: &SharedExecutionAbort) -> Option<ExecutionAbortReason> {
1210 shared
1211 .0
1212 .lock()
1213 .unwrap()
1214 .as_ref()
1215 .and_then(|state| state.reason)
1216}
1217
1218pub struct SessionManager {
1221 sessions: HashMap<String, SessionEntry>,
1222 quarantined: Vec<QuarantinedSession>,
1226 max_concurrency: Option<usize>,
1227 slot_control: SlotControl,
1228 event_tx: RuntimeEventSender,
1230 call_id_router: CallIdRouter,
1232 shared_call_id: SharedCallIdCounter,
1235 snapshot_cache: Arc<SnapshotCache>,
1237 warm_pool: Arc<WarmWorkerPool>,
1239 runtime: RuntimeContext,
1242 executor_teardown_timeout: Duration,
1243}
1244
1245struct QuarantinedSession {
1246 session_id: String,
1247 output_generation: Option<u64>,
1248 join_handle: thread::JoinHandle<()>,
1249 quarantined_at: Instant,
1250 deadline_reported: bool,
1251}
1252
1253impl SessionManager {
1254 pub fn new(
1255 max_concurrency: Option<usize>,
1256 event_tx: impl Into<RuntimeEventSender>,
1257 call_id_router: CallIdRouter,
1258 snapshot_cache: Arc<SnapshotCache>,
1259 runtime: RuntimeContext,
1260 ) -> Self {
1261 SessionManager {
1262 sessions: HashMap::new(),
1263 quarantined: Vec::new(),
1264 max_concurrency,
1265 slot_control: Arc::new((Mutex::new(0), Condvar::new())),
1266 event_tx: event_tx.into(),
1267 call_id_router,
1268 shared_call_id: Arc::new(AtomicU64::new(1)),
1269 snapshot_cache,
1270 warm_pool: Arc::new(WarmWorkerPool::default()),
1271 executor_teardown_timeout: runtime.vm_executor_teardown_timeout(),
1272 runtime,
1273 }
1274 }
1275
1276 #[cfg(test)]
1277 pub(crate) fn max_concurrency(&self) -> Option<usize> {
1278 self.max_concurrency
1279 }
1280
1281 #[allow(dead_code)]
1283 pub fn snapshot_cache(&self) -> &Arc<SnapshotCache> {
1284 &self.snapshot_cache
1285 }
1286
1287 pub fn pre_warm_workers(
1288 &self,
1289 bridge_code: String,
1290 userland_code: String,
1291 heap_limit_mb: Option<u32>,
1292 count: usize,
1293 ) {
1294 self.warm_pool.ensure_count(
1295 self.runtime.clone(),
1296 Arc::clone(&self.snapshot_cache),
1297 Arc::clone(&self.slot_control),
1298 bridge_code,
1299 userland_code,
1300 heap_limit_mb,
1301 count,
1302 );
1303 }
1304
1305 pub fn create_session(
1309 &mut self,
1310 session_id: String,
1311 heap_limit_mb: Option<u32>,
1312 cpu_time_limit_ms: Option<u32>,
1313 wall_clock_limit_ms: Option<u32>,
1314 ) -> Result<(), String> {
1315 self.create_session_with_output_generation(
1316 session_id,
1317 heap_limit_mb,
1318 cpu_time_limit_ms,
1319 wall_clock_limit_ms,
1320 None,
1321 None,
1322 )
1323 }
1324
1325 pub fn create_session_with_output_generation(
1326 &mut self,
1327 session_id: String,
1328 heap_limit_mb: Option<u32>,
1329 cpu_time_limit_ms: Option<u32>,
1330 wall_clock_limit_ms: Option<u32>,
1331 output_generation: Option<u64>,
1332 warm_hint: Option<WarmSessionHint>,
1333 ) -> Result<(), String> {
1334 self.create_session_with_output_generation_and_sender(
1335 session_id,
1336 heap_limit_mb,
1337 cpu_time_limit_ms,
1338 wall_clock_limit_ms,
1339 output_generation,
1340 warm_hint,
1341 None,
1342 )
1343 }
1344
1345 #[allow(clippy::too_many_arguments)]
1346 pub fn create_session_with_output_generation_and_sender(
1347 &mut self,
1348 session_id: String,
1349 heap_limit_mb: Option<u32>,
1350 cpu_time_limit_ms: Option<u32>,
1351 wall_clock_limit_ms: Option<u32>,
1352 output_generation: Option<u64>,
1353 warm_hint: Option<WarmSessionHint>,
1354 event_tx: Option<RuntimeEventSender>,
1355 ) -> Result<(), String> {
1356 let ready_batch_handle_limit = configured_resource_capacity(
1360 &self.runtime,
1361 ResourceClass::ReadyHandles,
1362 "limits.reactor.maxReadyHandles",
1363 "runtime.resources.maxReadyHandles",
1364 )?;
1365 self.create_session_with_output_generation_sender_and_runtime(
1366 session_id,
1367 heap_limit_mb,
1368 cpu_time_limit_ms,
1369 wall_clock_limit_ms,
1370 output_generation,
1371 warm_hint,
1372 event_tx,
1373 self.runtime.clone(),
1374 ready_batch_handle_limit,
1375 crate::host_call::DEFAULT_BRIDGE_CALL_TIMEOUT,
1376 )
1377 }
1378
1379 #[allow(clippy::too_many_arguments)]
1383 pub fn create_session_with_output_generation_sender_and_runtime(
1384 &mut self,
1385 session_id: String,
1386 heap_limit_mb: Option<u32>,
1387 cpu_time_limit_ms: Option<u32>,
1388 wall_clock_limit_ms: Option<u32>,
1389 output_generation: Option<u64>,
1390 warm_hint: Option<WarmSessionHint>,
1391 event_tx: Option<RuntimeEventSender>,
1392 session_runtime: RuntimeContext,
1393 ready_batch_handle_limit: usize,
1394 bridge_call_timeout: Duration,
1395 ) -> Result<(), String> {
1396 self.reap_finished_quarantines();
1397 if self.sessions.contains_key(&session_id) {
1398 return Err(format!("session {} already exists", session_id));
1399 }
1400
1401 let slot_permit = SessionSlotPermit::try_acquire(
1402 &self.slot_control,
1403 self.max_concurrency,
1404 self.runtime.metrics().clone(),
1405 )?;
1406
1407 let cpu_time_limit_ms = normalize_cpu_time_limit_ms(cpu_time_limit_ms);
1408 let wall_clock_limit_ms = normalize_wall_clock_limit_ms(wall_clock_limit_ms);
1409 let command_capacity = configured_resource_capacity(
1410 &session_runtime,
1411 ResourceClass::HandleCommands,
1412 "limits.reactor.maxHandleCommands",
1413 "runtime.resources.maxHandleCommands",
1414 )?;
1415 let (tx, rx) = crossbeam_channel::bounded(command_capacity);
1416 let (shutdown_tx, shutdown_rx) = crossbeam_channel::bounded(1);
1417 let ready_generation = output_generation.unwrap_or(1);
1418 let (ready_broker, ready_rx) =
1419 SessionReadiness::new(ready_generation, &session_runtime, ready_batch_handle_limit)?;
1420 let isolate_handle = Arc::new(Mutex::new(None));
1421 let execution_abort = new_execution_abort();
1422 let execution_active = Arc::new(AtomicBool::new(false));
1423 let pause_control = Arc::new(SessionPauseControl::default());
1424 #[cfg(test)]
1425 let session_resources = Arc::clone(session_runtime.resources());
1426 let assignment = SessionAssignment {
1427 heap_limit_mb,
1428 cpu_time_limit_ms,
1429 wall_clock_limit_ms,
1430 rx,
1431 shutdown_rx,
1432 ready_rx,
1433 ready_broker: Arc::clone(&ready_broker),
1434 slot_permit,
1435 event_tx: event_tx.unwrap_or_else(|| self.event_tx.clone()),
1436 call_id_router: Arc::clone(&self.call_id_router),
1437 shared_call_id: Arc::clone(&self.shared_call_id),
1438 snapshot_cache: Arc::clone(&self.snapshot_cache),
1439 isolate_handle: Arc::clone(&isolate_handle),
1440 execution_abort: execution_abort.clone(),
1441 execution_active: Arc::clone(&execution_active),
1442 pause_control: Arc::clone(&pause_control),
1443 session_id: session_id.clone(),
1444 output_generation,
1445 runtime: session_runtime,
1446 bridge_call_timeout,
1447 };
1448
1449 let join_handle = match self.claim_warm_worker(warm_hint.as_ref(), assignment) {
1450 Ok((join_handle, true)) => {
1451 if let Some(hint) = warm_hint {
1452 self.warm_pool.ensure_count(
1453 self.runtime.clone(),
1454 Arc::clone(&self.snapshot_cache),
1455 Arc::clone(&self.slot_control),
1456 hint.bridge_code,
1457 hint.userland_code,
1458 hint.heap_limit_mb,
1459 warm_worker_capacity_per_key(),
1460 );
1461 }
1462 join_handle
1463 }
1464 Ok((join_handle, false)) => join_handle,
1465 Err(assignment) => {
1466 if let Some(hint) = warm_hint {
1467 self.warm_pool.ensure_count(
1468 self.runtime.clone(),
1469 Arc::clone(&self.snapshot_cache),
1470 Arc::clone(&self.slot_control),
1471 hint.bridge_code,
1472 hint.userland_code,
1473 hint.heap_limit_mb,
1474 warm_worker_capacity_per_key(),
1475 );
1476 }
1477 spawn_session_thread(assignment)
1478 .map_err(|e| format!("failed to spawn session thread: {}", e))?
1479 }
1480 };
1481
1482 self.sessions.insert(
1483 session_id,
1484 SessionEntry {
1485 output_generation,
1486 tx,
1487 command_capacity,
1488 shutdown_tx,
1489 join_handle: Some(join_handle),
1490 isolate_handle,
1491 execution_abort,
1492 execution_active,
1493 pause_control,
1494 ready_broker,
1495 #[cfg(test)]
1496 session_resources,
1497 },
1498 );
1499
1500 Ok(())
1501 }
1502
1503 #[allow(clippy::result_large_err)]
1506 fn claim_warm_worker(
1507 &self,
1508 warm_hint: Option<&WarmSessionHint>,
1509 assignment: SessionAssignment,
1510 ) -> Result<(thread::JoinHandle<()>, bool), SessionAssignment> {
1511 let Some(hint) = warm_hint else {
1512 return Err(assignment);
1513 };
1514 if warm_worker_capacity_per_key() == 0 {
1515 record_warm_worker_miss();
1516 return Err(assignment);
1517 }
1518
1519 let key = warm_pool_key(&hint.bridge_code, &hint.userland_code, hint.heap_limit_mb);
1520 let Some(worker) = self.warm_pool.claim(&key) else {
1521 record_warm_worker_miss();
1522 eprintln!(
1523 "agentos-v8-runtime: warm worker pool-empty key={} heap={}",
1524 warm_key_prefix(&key),
1525 key.heap_limit_mb
1526 );
1527 return Err(assignment);
1528 };
1529
1530 match worker.assignment_tx.send(assignment) {
1531 Ok(()) => {
1532 record_warm_worker_hit();
1533 eprintln!(
1534 "agentos-v8-runtime: warm worker claimed key={} heap={}",
1535 warm_key_prefix(&key),
1536 key.heap_limit_mb
1537 );
1538 Ok((worker.join_handle, true))
1539 }
1540 Err(error) => {
1541 record_warm_worker_miss();
1542 let _ = worker.join_handle.join();
1543 Err(error.0)
1544 }
1545 }
1546 }
1547
1548 pub fn destroy_session_if_output_generation(
1549 &mut self,
1550 session_id: &str,
1551 output_generation: u64,
1552 ) -> Result<bool, String> {
1553 match self.begin_destroy_session_if_output_generation(session_id, output_generation)? {
1554 Some(shutdown) => {
1555 shutdown.finish();
1556 Ok(true)
1557 }
1558 None => Ok(false),
1559 }
1560 }
1561
1562 pub fn begin_destroy_session_if_output_generation(
1563 &mut self,
1564 session_id: &str,
1565 output_generation: u64,
1566 ) -> Result<Option<SessionShutdown>, String> {
1567 if self
1568 .sessions
1569 .get(session_id)
1570 .is_none_or(|entry| entry.output_generation != Some(output_generation))
1571 {
1572 return Ok(None);
1573 }
1574
1575 self.begin_destroy_session(session_id).map(Some)
1576 }
1577
1578 pub fn detach_session_if_output_generation(
1579 &mut self,
1580 session_id: &str,
1581 output_generation: u64,
1582 ) -> Result<bool, String> {
1583 if self
1584 .sessions
1585 .get(session_id)
1586 .is_none_or(|entry| entry.output_generation != Some(output_generation))
1587 {
1588 return Ok(false);
1589 }
1590
1591 self.detach_session(session_id)?;
1592 Ok(true)
1593 }
1594
1595 pub(crate) fn detach_session(&mut self, session_id: &str) -> Result<(), String> {
1596 let entry = self
1597 .sessions
1598 .get(session_id)
1599 .ok_or_else(|| format!("session {} does not exist", session_id))?;
1600 entry.pause_control.shutdown();
1601
1602 #[cfg(not(test))]
1603 if let Some(handle) = entry
1604 .isolate_handle
1605 .lock()
1606 .ok()
1607 .and_then(|guard| guard.as_ref().cloned())
1608 {
1609 handle.terminate_execution();
1610 }
1611 signal_execution_abort_durable(&entry.execution_abort, ExecutionAbortReason::Terminated);
1612 self.clear_call_routes_for_session(session_id, entry.output_generation);
1613 let mut entry = self.sessions.remove(session_id).unwrap();
1614 signal_session_shutdown(&entry.shutdown_tx, session_id);
1615 drop(entry.tx);
1616 if let Some(join_handle) = entry.join_handle.take() {
1617 eprintln!(
1618 "WARN_AGENTOS_VM_EXECUTOR_QUARANTINED: session={} generation={:?}",
1619 session_id, entry.output_generation
1620 );
1621 self.quarantined.push(QuarantinedSession {
1622 session_id: session_id.to_owned(),
1623 output_generation: entry.output_generation,
1624 join_handle,
1625 quarantined_at: Instant::now(),
1626 deadline_reported: false,
1627 });
1628 }
1629 Ok(())
1630 }
1631
1632 fn reap_finished_quarantines(&mut self) {
1633 let mut retained = Vec::with_capacity(self.quarantined.len());
1634 for mut quarantined in self.quarantined.drain(..) {
1635 if !quarantined.join_handle.is_finished() {
1636 if !quarantined.deadline_reported
1637 && quarantined.quarantined_at.elapsed() >= self.executor_teardown_timeout
1638 {
1639 quarantined.deadline_reported = true;
1640 eprintln!(
1641 "ERR_AGENTOS_VM_EXECUTOR_TEARDOWN_TIMEOUT: session={} generation={:?} deadline_ms={}; executor remains quarantined and retains its permit; raise runtime.executor.teardownTimeoutMs",
1642 quarantined.session_id,
1643 quarantined.output_generation,
1644 self.executor_teardown_timeout.as_millis()
1645 );
1646 }
1647 retained.push(quarantined);
1648 continue;
1649 }
1650 if quarantined.join_handle.join().is_err() {
1651 eprintln!(
1652 "ERR_AGENTOS_VM_EXECUTOR_PANIC: quarantined session={} generation={:?}",
1653 quarantined.session_id, quarantined.output_generation
1654 );
1655 } else {
1656 eprintln!(
1657 "INFO_AGENTOS_VM_EXECUTOR_QUARANTINE_RELEASED: session={} generation={:?}",
1658 quarantined.session_id, quarantined.output_generation
1659 );
1660 }
1661 self.call_id_router
1662 .cancel_session(&quarantined.session_id, quarantined.output_generation);
1663 }
1664 self.quarantined = retained;
1665 }
1666
1667 pub fn destroy_session(&mut self, session_id: &str) -> Result<(), String> {
1671 self.begin_destroy_session(session_id)?.finish();
1672 Ok(())
1673 }
1674
1675 pub fn begin_destroy_session(&mut self, session_id: &str) -> Result<SessionShutdown, String> {
1680 if !self.sessions.contains_key(session_id) {
1681 return Err(format!("session {} does not exist", session_id));
1682 }
1683
1684 let output_generation = self
1685 .sessions
1686 .get(session_id)
1687 .and_then(|entry| entry.output_generation);
1688 self.clear_call_routes_for_session(session_id, output_generation);
1689 let mut entry = self
1690 .sessions
1691 .remove(session_id)
1692 .expect("checked session exists");
1693 entry.pause_control.shutdown();
1694
1695 #[cfg(not(test))]
1696 if let Some(handle) = entry
1697 .isolate_handle
1698 .lock()
1699 .ok()
1700 .and_then(|guard| guard.as_ref().cloned())
1701 {
1702 handle.terminate_execution();
1703 }
1704 signal_execution_abort_durable(&entry.execution_abort, ExecutionAbortReason::Terminated);
1705 signal_session_shutdown(&entry.shutdown_tx, session_id);
1708 let join_handle = entry.join_handle.take();
1709 drop(entry);
1710 Ok(SessionShutdown {
1711 session_id: session_id.to_owned(),
1712 output_generation,
1713 join_handle,
1714 call_id_router: Arc::clone(&self.call_id_router),
1715 })
1716 }
1717
1718 pub(crate) fn take_session_shutdown_handles(&mut self) -> Vec<thread::JoinHandle<()>> {
1719 self.call_id_router.clear();
1720
1721 let mut handles: Vec<_> = self
1722 .sessions
1723 .drain()
1724 .filter_map(|(session_id, mut entry)| {
1725 #[cfg(not(test))]
1726 if let Some(handle) = entry
1727 .isolate_handle
1728 .lock()
1729 .ok()
1730 .and_then(|guard| guard.as_ref().cloned())
1731 {
1732 handle.terminate_execution();
1733 }
1734 signal_execution_abort_durable(
1735 &entry.execution_abort,
1736 ExecutionAbortReason::Terminated,
1737 );
1738 signal_session_shutdown(&entry.shutdown_tx, &session_id);
1739 drop(entry.tx);
1740 entry.join_handle.take()
1741 })
1742 .collect();
1743 handles.extend(self.warm_pool.shutdown_handles());
1744 handles.extend(
1745 self.quarantined
1746 .drain(..)
1747 .map(|quarantined| quarantined.join_handle),
1748 );
1749 handles
1750 }
1751
1752 #[cfg(test)]
1753 pub(crate) fn clear_call_route(&self, call_id: u64) {
1754 self.call_id_router.cancel(call_id);
1755 }
1756
1757 fn clear_call_routes_for_session(&self, session_id: &str, output_generation: Option<u64>) {
1758 self.call_id_router
1759 .cancel_session(session_id, output_generation);
1760 }
1761
1762 pub fn session_command_sender(
1767 &self,
1768 session_id: &str,
1769 msg: &SessionMessage,
1770 ) -> Result<(Sender<SessionCommand>, usize, Arc<AtomicBool>), String> {
1771 let entry = self
1772 .sessions
1773 .get(session_id)
1774 .ok_or_else(|| format!("session {} does not exist", session_id))?;
1775
1776 #[cfg(not(test))]
1777 if matches!(msg, SessionMessage::TerminateExecution) {
1778 if let Some(handle) = entry
1779 .isolate_handle
1780 .lock()
1781 .ok()
1782 .and_then(|guard| guard.as_ref().cloned())
1783 {
1784 handle.terminate_execution();
1785 }
1786 }
1787 if matches!(msg, SessionMessage::TerminateExecution) {
1788 if entry.execution_active.load(Ordering::Acquire) {
1789 signal_execution_abort_durable(
1790 &entry.execution_abort,
1791 ExecutionAbortReason::Terminated,
1792 );
1793 } else {
1794 signal_execution_abort(&entry.execution_abort, ExecutionAbortReason::Terminated);
1795 }
1796 }
1797
1798 Ok((
1799 entry.tx.clone(),
1800 entry.command_capacity,
1801 Arc::clone(&entry.execution_active),
1802 ))
1803 }
1804
1805 pub fn try_send_to_session(&self, session_id: &str, msg: SessionMessage) -> Result<(), String> {
1809 let terminate_requested = matches!(&msg, SessionMessage::TerminateExecution);
1810 let execute_requested = matches!(&msg, SessionMessage::Execute { .. });
1811 let incoming_kind = match &msg {
1812 SessionMessage::InjectGlobals { .. } => String::from("inject_globals"),
1813 SessionMessage::Execute { .. } => String::from("execute"),
1814 SessionMessage::BridgeResponse(_) => String::from("bridge_response"),
1815 SessionMessage::StreamEvent(event) => format!("stream_event:{}", event.event_type),
1816 SessionMessage::TerminateExecution => String::from("terminate_execution"),
1817 };
1818 let (sender, command_capacity, execution_active) =
1819 self.session_command_sender(session_id, &msg)?;
1820 if execute_requested {
1821 execution_active.store(true, Ordering::Release);
1822 }
1823 let command = SessionCommand::Message(msg);
1824
1825 match sender.try_send(command) {
1826 Ok(()) => Ok(()),
1827 Err(crossbeam_channel::TrySendError::Full(_)) if terminate_requested => {
1828 Ok(())
1831 }
1832 Err(crossbeam_channel::TrySendError::Full(_)) => {
1833 if execute_requested {
1834 execution_active.store(false, Ordering::Release);
1835 }
1836 Err(format!(
1837 "ERR_AGENTOS_SESSION_COMMAND_LIMIT: session {session_id} command queue exceeded limit of {command_capacity} while admitting {incoming_kind} (queued={}); raise limits.reactor.maxHandleCommands",
1838 sender.len()
1839 ))
1840 }
1841 Err(crossbeam_channel::TrySendError::Disconnected(_)) => {
1842 if execute_requested {
1843 execution_active.store(false, Ordering::Release);
1844 }
1845 Err(format!(
1846 "session thread disconnected for session {session_id}"
1847 ))
1848 }
1849 }
1850 }
1851
1852 pub fn publish_readiness(
1853 &self,
1854 session_id: &str,
1855 capability_id: u64,
1856 capability_generation: u64,
1857 flags: ReadyFlags,
1858 ) -> Result<(), String> {
1859 let entry = self
1860 .sessions
1861 .get(session_id)
1862 .ok_or_else(|| format!("session {session_id} does not exist"))?;
1863 entry
1864 .ready_broker
1865 .publish(capability_id, capability_generation, flags)
1866 }
1867
1868 pub fn publish_signal(&self, session_id: &str, signal: i32) -> Result<(), String> {
1869 let entry = self
1870 .sessions
1871 .get(session_id)
1872 .ok_or_else(|| format!("session {session_id} does not exist"))?;
1873 entry.ready_broker.publish_signal(signal)
1874 }
1875
1876 pub fn remove_readiness(
1877 &self,
1878 session_id: &str,
1879 capability_id: u64,
1880 capability_generation: u64,
1881 ) -> Result<(), String> {
1882 let entry = self
1883 .sessions
1884 .get(session_id)
1885 .ok_or_else(|| format!("session {session_id} does not exist"))?;
1886 entry
1887 .ready_broker
1888 .remove(capability_id, capability_generation)
1889 }
1890
1891 pub fn set_application_read_interest(
1892 &self,
1893 session_id: &str,
1894 capability_id: u64,
1895 capability_generation: u64,
1896 enabled: bool,
1897 ) -> Result<(), String> {
1898 let entry = self
1899 .sessions
1900 .get(session_id)
1901 .ok_or_else(|| format!("session {session_id} does not exist"))?;
1902 entry.ready_broker.set_application_read_interest(
1903 capability_id,
1904 capability_generation,
1905 enabled,
1906 )
1907 }
1908
1909 pub fn publish_timer(&self, session_id: &str, timer_id: u64) -> Result<(), String> {
1910 let entry = self
1911 .sessions
1912 .get(session_id)
1913 .ok_or_else(|| format!("session {session_id} does not exist"))?;
1914 entry.ready_broker.publish_timer(timer_id)
1915 }
1916
1917 pub fn session_sender(
1920 &self,
1921 session_id: &str,
1922 ) -> Result<(Sender<SessionCommand>, usize), String> {
1923 self.sessions
1924 .get(session_id)
1925 .map(|entry| (entry.tx.clone(), entry.command_capacity))
1926 .ok_or_else(|| format!("session {} does not exist", session_id))
1927 }
1928
1929 pub fn pause_session(&self, session_id: &str) -> Result<(), String> {
1933 let entry = self
1934 .sessions
1935 .get(session_id)
1936 .ok_or_else(|| format!("session {} does not exist", session_id))?;
1937 entry.pause_control.pause();
1938 #[cfg(not(test))]
1939 if let Some(handle) = entry
1940 .isolate_handle
1941 .lock()
1942 .ok()
1943 .and_then(|guard| guard.as_ref().cloned())
1944 {
1945 let raw = Arc::into_raw(Arc::clone(&entry.pause_control)) as *mut std::ffi::c_void;
1946 if !handle.request_interrupt(pause_isolate_interrupt, raw) {
1947 unsafe { drop(Arc::from_raw(raw.cast::<SessionPauseControl>())) };
1950 }
1951 }
1952 Ok(())
1953 }
1954
1955 pub fn resume_session(&self, session_id: &str) -> Result<(), String> {
1956 let entry = self
1957 .sessions
1958 .get(session_id)
1959 .ok_or_else(|| format!("session {} does not exist", session_id))?;
1960 entry.pause_control.resume();
1961 Ok(())
1962 }
1963
1964 pub fn send_to_session(&self, session_id: &str, msg: SessionMessage) -> Result<(), String> {
1966 self.try_send_to_session(session_id, msg)
1967 }
1968
1969 pub fn destroy_sessions<I>(&mut self, session_ids: I)
1973 where
1974 I: IntoIterator<Item = String>,
1975 {
1976 for shutdown in self.begin_destroy_sessions(session_ids) {
1977 shutdown.finish();
1978 }
1979 }
1980
1981 pub fn begin_destroy_sessions<I>(&mut self, session_ids: I) -> Vec<SessionShutdown>
1984 where
1985 I: IntoIterator<Item = String>,
1986 {
1987 session_ids
1988 .into_iter()
1989 .filter_map(|sid| self.begin_destroy_session(&sid).ok())
1990 .collect()
1991 }
1992
1993 #[allow(dead_code)]
1995 pub fn session_count(&self) -> usize {
1996 self.sessions.len()
1997 }
1998
1999 #[allow(dead_code)]
2000 pub fn quarantined_session_count(&mut self) -> usize {
2001 self.reap_finished_quarantines();
2002 self.quarantined.len()
2003 }
2004
2005 #[allow(dead_code)]
2007 pub fn all_sessions(&self) -> Vec<String> {
2008 self.sessions.keys().cloned().collect()
2009 }
2010
2011 #[allow(dead_code)]
2013 pub fn active_slot_count(&self) -> usize {
2014 let (lock, _) = &*self.slot_control;
2015 *lock.lock().unwrap()
2016 }
2017
2018 pub fn session_output_generation(&self, session_id: &str) -> Option<u64> {
2019 self.sessions
2020 .get(session_id)
2021 .and_then(|entry| entry.output_generation)
2022 }
2023
2024 #[cfg(test)]
2025 pub fn session_resources(
2026 &self,
2027 session_id: &str,
2028 ) -> Option<Arc<agentos_runtime::accounting::ResourceLedger>> {
2029 self.sessions
2030 .get(session_id)
2031 .map(|entry| Arc::clone(&entry.session_resources))
2032 }
2033
2034 pub fn call_id_router(&self) -> &CallIdRouter {
2036 &self.call_id_router
2037 }
2038}
2039
2040#[cfg(not(test))]
2042fn send_event_with_generation(
2043 event_tx: &RuntimeEventSender,
2044 output_generation: Option<u64>,
2045 event: RuntimeEvent,
2046) {
2047 if let Err(error) = event_tx.send(RuntimeEventEnvelope {
2048 output_generation,
2049 event,
2050 }) {
2051 eprintln!("failed to send runtime event: {error}");
2052 }
2053}
2054
2055fn send_late_message_warning(
2056 event_tx: &RuntimeEventSender,
2057 session_id: &str,
2058 output_generation: Option<u64>,
2059 error_code: &str,
2060 detail: String,
2061) {
2062 let warning = RuntimeEvent::Log {
2063 session_id: session_id.to_string(),
2064 channel: 1,
2065 message: format!("[{error_code}] {detail}"),
2066 };
2067 if let Err(error) = event_tx.send(RuntimeEventEnvelope {
2068 output_generation,
2069 event: warning,
2070 }) {
2071 eprintln!("failed to send late-session warning: {error}");
2072 }
2073}
2074
2075fn handle_late_session_message(
2076 event_tx: &RuntimeEventSender,
2077 session_id: &str,
2078 output_generation: Option<u64>,
2079 message: SessionMessage,
2080) {
2081 match message {
2082 SessionMessage::BridgeResponse(BridgeResponse {
2083 call_id,
2084 status,
2085 payload,
2086 reservation: _,
2087 }) => send_late_message_warning(
2088 event_tx,
2089 session_id,
2090 output_generation,
2091 LATE_BRIDGE_RESPONSE_ERROR_CODE,
2092 format!(
2093 "dropping BridgeResponse after execution completed (call_id={call_id}, status={status}, payload_len={})",
2094 payload.len()
2095 ),
2096 ),
2097 SessionMessage::StreamEvent(StreamEvent {
2098 event_type,
2099 payload,
2100 }) => {
2101 if event_type == "timer" || event_type == "net_socket" || event_type == "stdin_end" {
2107 return;
2108 }
2109 send_late_message_warning(
2110 event_tx,
2111 session_id,
2112 output_generation,
2113 LATE_STREAM_EVENT_ERROR_CODE,
2114 format!(
2115 "dropping StreamEvent after execution completed (event_type={event_type}, payload_len={})",
2116 payload.len()
2117 ),
2118 )
2119 }
2120 SessionMessage::TerminateExecution => send_late_message_warning(
2121 event_tx,
2122 session_id,
2123 output_generation,
2124 LATE_TERMINATE_EXECUTION_ERROR_CODE,
2125 String::from("dropping TerminateExecution after execution completed"),
2126 ),
2127 SessionMessage::InjectGlobals { .. } | SessionMessage::Execute { .. } => {}
2128 }
2129}
2130
2131#[cfg(not(test))]
2132fn install_wasm_module_bytes_global<'s>(scope: &mut v8::HandleScope<'s>, bytes: &[u8]) -> bool {
2133 let global = scope.get_current_context().global(scope);
2134 let Some(name) = v8::String::new(scope, "__agentOSWasmModuleBytes") else {
2135 return false;
2136 };
2137 let len = bytes.len();
2138 let backing_store = v8::ArrayBuffer::new_backing_store_from_bytes(bytes.to_vec());
2139 let array_buffer = v8::ArrayBuffer::with_backing_store(scope, &backing_store.make_shared());
2140 let Some(bytes_value) = v8::Uint8Array::new(scope, array_buffer, 0, len) else {
2141 return false;
2142 };
2143 global.set(scope, name.into(), bytes_value.into()).is_some()
2144}
2145
2146fn spawn_session_thread(assignment: SessionAssignment) -> std::io::Result<thread::JoinHandle<()>> {
2150 let name_prefix = if assignment.session_id.len() > 8 {
2151 assignment.session_id[..8].to_string()
2152 } else {
2153 assignment.session_id.clone()
2154 };
2155 thread::Builder::new()
2157 .name(format!("session-{}", name_prefix))
2158 .spawn(move || session_thread(assignment, None))
2159}
2160
2161fn recv_session_command(
2162 rx: &Receiver<SessionCommand>,
2163 shutdown_rx: &Receiver<()>,
2164 ready_rx: &Receiver<ReadyWake>,
2165 ready_broker: &SessionReadiness,
2166) -> Option<SessionCommand> {
2167 loop {
2168 match shutdown_rx.try_recv() {
2175 Ok(()) | Err(crossbeam_channel::TryRecvError::Disconnected) => {
2176 return Some(SessionCommand::Shutdown);
2177 }
2178 Err(crossbeam_channel::TryRecvError::Empty) => {}
2179 }
2180 match rx.try_recv() {
2181 Ok(command) => return Some(command),
2182 Err(crossbeam_channel::TryRecvError::Disconnected) => return None,
2183 Err(crossbeam_channel::TryRecvError::Empty) => {}
2184 }
2185 match ready_rx.try_recv() {
2186 Ok(wake) => match ready_batch_command(ready_broker, wake) {
2187 Ok(command) => return Some(command),
2188 Err(error) => {
2189 eprintln!("{error}");
2190 continue;
2191 }
2192 },
2193 Err(crossbeam_channel::TryRecvError::Disconnected) => return None,
2194 Err(crossbeam_channel::TryRecvError::Empty) => {}
2195 }
2196
2197 crossbeam_channel::select! {
2198 recv(shutdown_rx) -> _ => return Some(SessionCommand::Shutdown),
2199 recv(ready_rx) -> wake => {
2200 let wake = wake.ok()?;
2201 match ready_batch_command(ready_broker, wake) {
2202 Ok(command) => return Some(command),
2203 Err(error) => {
2204 eprintln!("{error}");
2205 continue;
2206 }
2207 }
2208 },
2209 recv(rx) -> command => return command.ok(),
2210 }
2211 }
2212}
2213
2214fn ready_batch_command(
2218 ready_broker: &SessionReadiness,
2219 wake: ReadyWake,
2220) -> Result<SessionCommand, String> {
2221 ready_broker
2222 .take_batch(wake)
2223 .map(SessionCommand::ReadyBatch)
2224}
2225
2226#[allow(clippy::too_many_arguments)]
2227fn session_thread(
2228 assignment: SessionAssignment,
2229 #[cfg_attr(test, allow(unused_variables))] precreated_isolate: Option<PrecreatedIsolate>,
2230) {
2231 let SessionAssignment {
2232 heap_limit_mb,
2233 cpu_time_limit_ms,
2234 wall_clock_limit_ms,
2235 rx,
2236 shutdown_rx,
2237 ready_rx,
2238 ready_broker,
2239 slot_permit: _slot_permit,
2240 event_tx,
2241 call_id_router,
2242 shared_call_id,
2243 snapshot_cache,
2244 isolate_handle,
2245 execution_abort,
2246 execution_active,
2247 pause_control,
2248 session_id,
2249 output_generation,
2250 runtime,
2251 bridge_call_timeout,
2252 } = assignment;
2253 #[cfg(not(test))]
2254 let execution_task_owner =
2255 output_generation.map(|generation| agentos_runtime::TaskOwner::Vm { generation });
2256 #[cfg(test)]
2257 let _ = (
2258 heap_limit_mb,
2259 cpu_time_limit_ms,
2260 wall_clock_limit_ms,
2261 call_id_router,
2262 shared_call_id,
2263 snapshot_cache,
2264 isolate_handle,
2265 execution_abort,
2266 &pause_control,
2267 &ready_broker,
2268 &runtime,
2269 bridge_call_timeout,
2270 );
2271
2272 #[cfg(all(not(test), unix))]
2278 let exec_thread_cpu_clock = crate::timeout::current_thread_cpu_clock();
2279 #[cfg(all(not(test), not(unix)))]
2280 let exec_thread_cpu_clock: Option<crate::timeout::ThreadCpuClock> = None;
2281
2282 #[cfg(not(test))]
2286 let (
2287 mut v8_isolate,
2288 mut _v8_context,
2289 mut from_snapshot,
2290 mut isolate_bridge_code,
2291 mut isolate_userland_code,
2292 ) = match precreated_isolate {
2293 Some(mut precreated) => (
2294 precreated.isolate.take(),
2295 precreated.context.take(),
2296 true,
2297 Some(std::mem::take(&mut precreated.bridge_code)),
2298 Some(std::mem::take(&mut precreated.userland_code)),
2299 ),
2300 None => (None, None, false, None, None),
2301 };
2302
2303 #[cfg(not(test))]
2304 let mut pending = bridge::PendingPromises::new();
2305
2306 #[cfg(not(test))]
2308 let mut last_globals_payload: Option<Vec<u8>> = None;
2309
2310 #[cfg(not(test))]
2312 let mut bridge_cache: Option<execution::BridgeCodeCache> = None;
2313
2314 #[cfg(not(test))]
2316 let mut last_bridge_code: Option<String> = None;
2317 #[cfg(not(test))]
2319 let mut last_userland_code: Option<String> = None;
2320
2321 #[cfg(not(test))]
2327 let mut high_resolution_time_origin = Instant::now();
2328
2329 #[cfg(not(test))]
2330 if let Some(iso) = v8_isolate.as_mut() {
2331 *isolate_handle
2332 .lock()
2333 .expect("session isolate handle lock poisoned") = Some(iso.thread_safe_handle());
2334 }
2335
2336 loop {
2338 let next_command = recv_session_command(&rx, &shutdown_rx, &ready_rx, &ready_broker);
2339
2340 pause_control.wait_while_paused();
2341 match next_command {
2342 Some(SessionCommand::Shutdown) | None => break,
2343 Some(SessionCommand::ReadyBatch(batch)) => {
2344 if batch.signals_ready {
2345 if let Err(error) = ready_broker.drain_signals(&batch) {
2346 eprintln!("ERR_AGENTOS_READY_DISCARD: could not discard signals: {error}");
2347 }
2348 }
2349 if batch.timers_ready {
2350 if let Err(error) = ready_broker.drain_timers(&batch) {
2351 eprintln!("ERR_AGENTOS_READY_DISCARD: could not discard timers: {error}");
2352 }
2353 }
2354 if let Err(error) = ready_broker.complete_batch(&batch, &batch.entries) {
2355 eprintln!("{error}");
2356 }
2357 }
2358 Some(SessionCommand::SetModuleReader(reader)) => {
2359 execution::install_session_guest_reader(Some(reader));
2360 }
2361 Some(SessionCommand::Message(msg)) => match msg {
2362 SessionMessage::InjectGlobals { payload } => {
2363 #[cfg(not(test))]
2364 {
2365 last_globals_payload = Some(payload);
2367 }
2368 #[cfg(test)]
2369 {
2370 let _ = payload;
2371 }
2372 }
2373 SessionMessage::Execute {
2374 mode,
2375 file_path,
2376 bridge_code,
2377 post_restore_script,
2378 userland_code,
2379 high_resolution_time,
2380 user_code,
2381 wasm_module_bytes,
2382 } => {
2383 let _execution_activity = ExecutionActivityGuard(Arc::clone(&execution_active));
2384 #[cfg(test)]
2387 let _ = &userland_code;
2388 #[cfg(test)]
2389 let _ = high_resolution_time;
2390 #[cfg(test)]
2391 let _ = &wasm_module_bytes;
2392 #[cfg(not(test))]
2393 {
2394 let session_id = session_id.clone();
2395 let should_update_cached_bridge_code = !bridge_code.is_empty();
2397 let effective_bridge_code = if bridge_code.is_empty() {
2398 last_bridge_code.as_deref().unwrap_or("").to_string()
2399 } else {
2400 bridge_code
2401 };
2402 let should_update_cached_userland_code = !userland_code.is_empty();
2404 let effective_userland_code = if userland_code.is_empty() {
2405 last_userland_code.as_deref().unwrap_or("").to_string()
2406 } else {
2407 userland_code
2408 };
2409
2410 if let Err(message) =
2411 snapshot::validate_bridge_code_size(&effective_bridge_code)
2412 {
2413 let result_frame = RuntimeEvent::ExecutionResult {
2414 session_id,
2415 exit_code: 1,
2416 exports: None,
2417 limit_exceeded: None,
2418 error: Some(ExecutionErrorBin {
2419 error_type: "Error".into(),
2420 message,
2421 stack: String::new(),
2422 code: snapshot::V8_BRIDGE_CODE_LIMIT_ERROR_CODE.into(),
2423 }),
2424 };
2425 send_event_with_generation(&event_tx, output_generation, result_frame);
2426 continue;
2427 }
2428
2429 if should_update_cached_bridge_code {
2430 last_bridge_code = Some(effective_bridge_code.clone());
2431 }
2432 if should_update_cached_userland_code {
2433 last_userland_code = Some(effective_userland_code.clone());
2434 }
2435
2436 if v8_isolate.is_some()
2437 && (isolate_bridge_code.as_deref()
2438 != Some(effective_bridge_code.as_str())
2439 || isolate_userland_code.as_deref()
2440 != Some(effective_userland_code.as_str()))
2441 {
2442 *isolate_handle
2443 .lock()
2444 .expect("session isolate handle lock poisoned") = None;
2445 reset_pending_promises(&mut pending);
2452 drop(_v8_context.take());
2453 isolate::drop_isolate(v8_isolate.take());
2454 from_snapshot = false;
2455 isolate_bridge_code = None;
2456 isolate_userland_code = None;
2457 }
2458
2459 if v8_isolate.is_none() {
2461 isolate::init_v8_platform();
2462 let phase_start = Instant::now();
2466 let snapshot_blob = match snapshot_cache.get_or_create_with_userland(
2467 &effective_bridge_code,
2468 (!effective_userland_code.is_empty())
2469 .then_some(effective_userland_code.as_str()),
2470 ) {
2471 Ok(blob) => Some(blob),
2472 Err(message) => {
2473 eprintln!(
2478 "agentos-v8-runtime: snapshot creation failed, \
2479 falling back to fresh isolate: {message}"
2480 );
2481 None
2482 }
2483 };
2484 record_v8_session_phase("snapshot_get", phase_start.elapsed());
2485 let mut iso = match snapshot_blob {
2486 Some(blob) => {
2487 from_snapshot = true;
2488 eprintln!(
2489 "agentos-v8-runtime: restored session isolate from_snapshot=true"
2490 );
2491 let phase_start = Instant::now();
2492 let snapshot_blob = (*blob).clone();
2496 record_v8_session_phase("blob_clone", phase_start.elapsed());
2497 let phase_start = Instant::now();
2498 let isolate = snapshot::create_isolate_from_snapshot(
2499 snapshot_blob,
2500 heap_limit_mb,
2501 );
2502 record_v8_session_phase("isolate_new", phase_start.elapsed());
2503 isolate
2504 }
2505 None => {
2506 from_snapshot = false;
2507 let phase_start = Instant::now();
2508 let isolate = isolate::create_isolate(heap_limit_mb);
2509 record_v8_session_phase("isolate_new", phase_start.elapsed());
2510 isolate
2511 }
2512 };
2513 iso.set_host_import_module_dynamically_callback(
2514 execution::dynamic_import_callback,
2515 );
2516 iso.set_host_initialize_import_meta_object_callback(
2517 execution::import_meta_object_callback,
2518 );
2519 high_resolution_time_origin = Instant::now();
2520 *isolate_handle
2521 .lock()
2522 .expect("session isolate handle lock poisoned") =
2523 Some(iso.thread_safe_handle());
2524 let ctx = isolate::create_context(&mut iso);
2525 _v8_context = Some(ctx);
2526 v8_isolate = Some(iso);
2527 isolate_bridge_code = Some(effective_bridge_code.clone());
2528 isolate_userland_code = Some(effective_userland_code.clone());
2529 }
2530
2531 let iso = v8_isolate.as_mut().unwrap();
2532 iso.cancel_terminate_execution();
2533
2534 let retain_context = mode & 2 != 0;
2538 let module_mode = mode & 1 != 0;
2539 let exec_context = if retain_context {
2540 _v8_context
2541 .as_ref()
2542 .expect("session context exists after isolate creation")
2543 .clone()
2544 } else {
2545 isolate::create_context(iso)
2546 };
2547
2548 if high_resolution_time {
2549 let scope = &mut v8::HandleScope::new(iso);
2550 let ctx = v8::Local::new(scope, &exec_context);
2551 let scope = &mut v8::ContextScope::new(scope, ctx);
2552 execution::install_high_resolution_time_global(
2553 scope,
2554 &high_resolution_time_origin as *const Instant,
2555 );
2556 }
2557
2558 if let Some(ref payload) = last_globals_payload {
2560 let scope = &mut v8::HandleScope::new(iso);
2561 let ctx = v8::Local::new(scope, &exec_context);
2562 let scope = &mut v8::ContextScope::new(scope, ctx);
2563 if let Err(error) =
2564 execution::inject_globals_from_payload(scope, payload)
2565 {
2566 let result_frame = RuntimeEvent::ExecutionResult {
2567 session_id,
2568 exit_code: 1,
2569 exports: None,
2570 limit_exceeded: None,
2571 error: Some(ExecutionErrorBin {
2572 error_type: error.error_type,
2573 message: error.message,
2574 stack: error.stack,
2575 code: error.code.unwrap_or_default(),
2576 }),
2577 };
2578 send_event_with_generation(
2579 &event_tx,
2580 output_generation,
2581 result_frame,
2582 );
2583 continue;
2584 }
2585 }
2586
2587 let (_active_execution_abort, abort_rx) =
2590 ActiveExecutionAbort::arm(&execution_abort);
2591 if execution_abort_requested(&abort_rx) {
2592 send_event_with_generation(
2597 &event_tx,
2598 output_generation,
2599 RuntimeEvent::ExecutionResult {
2600 session_id,
2601 exit_code: 1,
2602 exports: None,
2603 limit_exceeded: None,
2604 error: Some(ExecutionErrorBin {
2605 error_type: String::from("Error"),
2606 message: String::from("Execution terminated"),
2607 stack: String::new(),
2608 code: String::new(),
2609 }),
2610 },
2611 );
2612 continue;
2613 }
2614
2615 let (async_response_tx, async_response_rx) =
2619 crossbeam_channel::bounded(bridge::MAX_PENDING_PROMISES);
2620 let bridge_ctx = BridgeCallContext::with_registry(
2621 Box::new(ChannelRuntimeEventSender::new(
2622 event_tx.clone(),
2623 output_generation,
2624 )),
2625 session_id.clone(),
2626 output_generation,
2627 Arc::clone(&call_id_router),
2628 Arc::clone(&shared_call_id),
2629 async_response_tx,
2630 abort_rx.clone(),
2631 runtime.clone(),
2632 Arc::clone(&pause_control),
2633 bridge_call_timeout,
2634 );
2635
2636 let _sync_store;
2640 let _async_store;
2641 let sync_bridge_fns = sync_bridge_fns();
2642 let async_bridge_fns = async_bridge_fns();
2643 {
2644 let scope = &mut v8::HandleScope::new(iso);
2645 let ctx = v8::Local::new(scope, &exec_context);
2646 let scope = &mut v8::ContextScope::new(scope, ctx);
2647
2648 (_sync_store, _async_store) = bridge::replace_bridge_fns(
2649 scope,
2650 &bridge_ctx as *const BridgeCallContext,
2651 &pending as *const bridge::PendingPromises,
2652 sync_bridge_fns,
2653 async_bridge_fns,
2654 );
2655 }
2656
2657 if !post_restore_script.is_empty() {
2660 let scope = &mut v8::HandleScope::new(iso);
2661 let ctx = v8::Local::new(scope, &exec_context);
2662 let scope = &mut v8::ContextScope::new(scope, ctx);
2663 let (prs_code, prs_err) =
2664 execution::run_init_script(scope, &post_restore_script);
2665 if prs_code != 0 {
2666 let result_frame = RuntimeEvent::ExecutionResult {
2667 session_id,
2668 exit_code: prs_code,
2669 exports: None,
2670 limit_exceeded: None,
2671 error: prs_err.map(|e| ExecutionErrorBin {
2672 error_type: e.error_type,
2673 message: e.message,
2674 stack: e.stack,
2675 code: e.code.unwrap_or_default(),
2676 }),
2677 };
2678 send_event_with_generation(
2679 &event_tx,
2680 output_generation,
2681 result_frame,
2682 );
2683 continue;
2684 }
2685 }
2686
2687 if let Some(wasm_module_bytes) = wasm_module_bytes.as_ref() {
2688 let scope = &mut v8::HandleScope::new(iso);
2689 let ctx = v8::Local::new(scope, &exec_context);
2690 let scope = &mut v8::ContextScope::new(scope, ctx);
2691 if !install_wasm_module_bytes_global(scope, wasm_module_bytes) {
2692 let result_frame = RuntimeEvent::ExecutionResult {
2693 session_id,
2694 exit_code: 1,
2695 exports: None,
2696 limit_exceeded: None,
2697 error: Some(ExecutionErrorBin {
2698 error_type: "Error".into(),
2699 message: "failed to install __agentOSWasmModuleBytes"
2700 .into(),
2701 stack: String::new(),
2702 code: String::new(),
2703 }),
2704 };
2705 send_event_with_generation(
2706 &event_tx,
2707 output_generation,
2708 result_frame,
2709 );
2710 continue;
2711 }
2712 }
2713
2714 let mut cpu_budget_guard = match cpu_time_limit_ms {
2726 Some(budget_ms) => {
2727 let cpu_clock = match exec_thread_cpu_clock {
2733 Some(clock) => clock,
2734 None => {
2735 let result_frame = RuntimeEvent::ExecutionResult {
2736 session_id,
2737 exit_code: 1,
2738 exports: None,
2739 limit_exceeded: None,
2740 error: Some(ExecutionErrorBin {
2741 error_type: "Error".into(),
2742 message: format!(
2743 "{}: per-thread CPU clock unavailable; cannot enforce limits.jsRuntime.cpuTimeLimitMs",
2744 crate::timeout::CPU_BUDGET_GUARD_START_ERROR_CODE
2745 ),
2746 stack: String::new(),
2747 code: crate::timeout::CPU_BUDGET_GUARD_START_ERROR_CODE
2748 .into(),
2749 }),
2750 };
2751 send_event_with_generation(
2752 &event_tx,
2753 output_generation,
2754 result_frame,
2755 );
2756 continue;
2757 }
2758 };
2759 let handle = iso.thread_safe_handle();
2760 match crate::timeout::CpuBudgetGuard::new(
2761 &runtime,
2762 execution_task_owner.clone(),
2763 budget_ms,
2764 cpu_clock,
2765 handle,
2766 execution_abort.clone(),
2767 ) {
2768 Ok(guard) => Some(guard),
2769 Err(message) => {
2770 let result_frame = RuntimeEvent::ExecutionResult {
2771 session_id,
2772 exit_code: 1,
2773 exports: None,
2774 limit_exceeded: None,
2775 error: Some(ExecutionErrorBin {
2776 error_type: "Error".into(),
2777 message,
2778 stack: String::new(),
2779 code:
2780 crate::timeout::CPU_BUDGET_GUARD_START_ERROR_CODE
2781 .into(),
2782 }),
2783 };
2784 send_event_with_generation(
2785 &event_tx,
2786 output_generation,
2787 result_frame,
2788 );
2789 continue;
2790 }
2791 }
2792 }
2793 _ => None,
2794 };
2795
2796 let mut wall_clock_guard = match wall_clock_limit_ms {
2806 Some(limit_ms) => {
2807 let handle = iso.thread_safe_handle();
2808 match crate::timeout::TimeoutGuard::with_execution_abort(
2809 &runtime,
2810 execution_task_owner.clone(),
2811 limit_ms,
2812 handle,
2813 execution_abort.clone(),
2814 ) {
2815 Ok(guard) => Some(guard),
2816 Err(message) => {
2817 let result_frame = RuntimeEvent::ExecutionResult {
2818 session_id,
2819 exit_code: 1,
2820 exports: None,
2821 limit_exceeded: None,
2822 error: Some(ExecutionErrorBin {
2823 error_type: "Error".into(),
2824 message,
2825 stack: String::new(),
2826 code:
2827 crate::timeout::TIMEOUT_GUARD_START_ERROR_CODE
2828 .into(),
2829 }),
2830 };
2831 send_event_with_generation(
2832 &event_tx,
2833 output_generation,
2834 result_frame,
2835 );
2836 continue;
2837 }
2838 }
2839 }
2840 _ => None,
2841 };
2842
2843 let bridge_code_for_exec = if from_snapshot {
2847 ""
2848 } else {
2849 &effective_bridge_code
2850 };
2851 let file_path_opt = if file_path.is_empty() {
2852 None
2853 } else {
2854 Some(file_path.as_str())
2855 };
2856 let phase_start = Instant::now();
2857 let (mut code, mut exports, mut error) = if !module_mode {
2858 let scope = &mut v8::HandleScope::new(iso);
2859 let ctx = v8::Local::new(scope, &exec_context);
2860 let scope = &mut v8::ContextScope::new(scope, ctx);
2861 let (c, e) = execution::execute_script_with_options(
2862 scope,
2863 Some(&bridge_ctx),
2864 bridge_code_for_exec,
2865 &user_code,
2866 file_path_opt,
2867 &mut bridge_cache,
2868 );
2869 (c, None, e)
2870 } else {
2871 let scope = &mut v8::HandleScope::new(iso);
2872 let ctx = v8::Local::new(scope, &exec_context);
2873 let scope = &mut v8::ContextScope::new(scope, ctx);
2874 execution::execute_module(
2875 scope,
2876 &bridge_ctx,
2877 bridge_code_for_exec,
2878 &user_code,
2879 file_path_opt,
2880 &mut bridge_cache,
2881 )
2882 };
2883
2884 if module_mode && error.is_none() {
2888 let scope = &mut v8::HandleScope::new(iso);
2889 let ctx = v8::Local::new(scope, &exec_context);
2890 let scope = &mut v8::ContextScope::new(scope, ctx);
2891 if let Some((next_code, next_exports, next_error)) =
2892 execution::finalize_pending_module_evaluation(scope)
2893 {
2894 code = next_code;
2895 exports = next_exports;
2896 error = next_error;
2897 }
2898 }
2899 record_v8_session_phase("user_code_execute", phase_start.elapsed());
2900
2901 let should_enter_event_loop = !pending.is_empty()
2908 || execution::has_pending_module_evaluation()
2909 || execution::has_pending_script_evaluation();
2910 let event_loop_status = if should_enter_event_loop {
2911 let scope = &mut v8::HandleScope::new(iso);
2912 let ctx = v8::Local::new(scope, &exec_context);
2913 let scope = &mut v8::ContextScope::new(scope, ctx);
2914 run_event_loop_with_readiness(
2915 scope,
2916 EventLoopSources {
2917 commands: &rx,
2918 readiness: &ready_broker,
2919 readiness_wakes: &ready_rx,
2920 bridge_responses: Some(&async_response_rx),
2921 abort: Some(&abort_rx),
2922 pause: Some(&pause_control),
2923 },
2924 &pending,
2925 )
2926 } else {
2927 EventLoopStatus::Completed
2928 };
2929
2930 let mut terminated =
2931 matches!(event_loop_status, EventLoopStatus::Terminated);
2932 if let EventLoopStatus::Failed(next_code, next_error) = event_loop_status {
2933 code = next_code;
2934 error = Some(next_error);
2935 }
2936
2937 if !terminated && module_mode && error.is_none() {
2940 let scope = &mut v8::HandleScope::new(iso);
2941 let ctx = v8::Local::new(scope, &exec_context);
2942 let scope = &mut v8::ContextScope::new(scope, ctx);
2943 if let Some((next_code, next_exports, next_error)) =
2944 execution::finalize_pending_module_evaluation(scope)
2945 {
2946 code = next_code;
2947 exports = next_exports;
2948 error = next_error;
2949 }
2950 }
2951
2952 if !terminated && error.is_none() {
2957 if execution_abort_requested(&abort_rx) {
2961 terminated = true;
2962 } else {
2963 {
2968 let scope = &mut v8::HandleScope::new(iso);
2969 let ctx = v8::Local::new(scope, &exec_context);
2970 let scope = &mut v8::ContextScope::new(scope, ctx);
2971 let global = ctx.global(scope);
2972 let key =
2973 v8::String::new(scope, "_waitForActiveHandles").unwrap();
2974 if let Some(func) = global.get(scope, key.into()) {
2975 if func.is_function() {
2976 let func =
2977 v8::Local::<v8::Function>::try_from(func).unwrap();
2978 let recv = v8::undefined(scope).into();
2979 if let Some(result) = func.call(scope, recv, &[]) {
2980 if result.is_promise() {
2981 let promise =
2982 v8::Local::<v8::Promise>::try_from(result)
2983 .unwrap();
2984 if promise.state() == v8::PromiseState::Pending
2985 {
2986 execution::set_pending_script_evaluation(
2987 scope, promise,
2988 );
2989 }
2990 }
2991 }
2992 }
2993 }
2994 }
2995
2996 if !pending.is_empty() || execution::has_pending_script_evaluation()
2998 {
2999 let scope = &mut v8::HandleScope::new(iso);
3000 let ctx = v8::Local::new(scope, &exec_context);
3001 let scope = &mut v8::ContextScope::new(scope, ctx);
3002 let event_loop_status = run_event_loop_with_readiness(
3003 scope,
3004 EventLoopSources {
3005 commands: &rx,
3006 readiness: &ready_broker,
3007 readiness_wakes: &ready_rx,
3008 bridge_responses: Some(&async_response_rx),
3009 abort: Some(&abort_rx),
3010 pause: Some(&pause_control),
3011 },
3012 &pending,
3013 );
3014
3015 if matches!(event_loop_status, EventLoopStatus::Terminated) {
3016 terminated = true;
3017 }
3018 if let EventLoopStatus::Failed(next_code, next_error) =
3019 event_loop_status
3020 {
3021 code = next_code;
3022 error = Some(next_error);
3023 }
3024 }
3025 }
3026 }
3027
3028 if !terminated && !module_mode && error.is_none() {
3029 let scope = &mut v8::HandleScope::new(iso);
3030 let ctx = v8::Local::new(scope, &exec_context);
3031 let scope = &mut v8::ContextScope::new(scope, ctx);
3032 if let Some((next_code, next_error)) =
3033 execution::finalize_pending_script_evaluation(scope)
3034 {
3035 code = next_code;
3036 error = next_error;
3037 }
3038 }
3039
3040 if !terminated && error.is_none() {
3045 let scope = &mut v8::HandleScope::new(iso);
3046 let ctx = v8::Local::new(scope, &exec_context);
3047 let scope = &mut v8::ContextScope::new(scope, ctx);
3048 if let Some(process_exit_code) =
3049 execution::extract_global_process_exit_code(scope)
3050 {
3051 code = process_exit_code;
3052 }
3053 }
3054
3055 let abort_reason = execution_abort_reason(&execution_abort);
3061 let wall_clock_timed_out =
3062 wall_clock_guard.as_ref().is_some_and(|g| g.timed_out())
3063 || matches!(
3064 abort_reason,
3065 Some(ExecutionAbortReason::WallClockTimedOut)
3066 );
3067 let cpu_budget_exceeded =
3068 cpu_budget_guard.as_ref().is_some_and(|g| g.exceeded())
3069 || matches!(
3070 abort_reason,
3071 Some(ExecutionAbortReason::CpuBudgetExceeded)
3072 );
3073 let cpu_budget_exceeded = cpu_budget_exceeded
3077 && !matches!(
3078 abort_reason,
3079 Some(ExecutionAbortReason::WallClockTimedOut)
3080 );
3081 let wall_clock_timed_out = wall_clock_timed_out && !cpu_budget_exceeded;
3082
3083 if let Some(ref mut guard) = cpu_budget_guard {
3085 guard.cancel();
3086 }
3087 drop(cpu_budget_guard);
3088 if let Some(ref mut guard) = wall_clock_guard {
3089 guard.cancel();
3090 }
3091 drop(wall_clock_guard);
3092
3093 if matches!(abort_reason, Some(ExecutionAbortReason::Terminated)) {
3094 terminated = true;
3095 code = 1;
3096 exports = None;
3097 error = None;
3098 }
3099 let heap_limit_exceeded = isolate::take_heap_limit_exceeded(iso);
3103 if terminated
3104 || cpu_budget_exceeded
3105 || wall_clock_timed_out
3106 || heap_limit_exceeded
3107 {
3108 iso.cancel_terminate_execution();
3109 }
3110
3111 let result_frame = if heap_limit_exceeded {
3113 RuntimeEvent::ExecutionResult {
3114 session_id,
3115 exit_code: 1,
3116 exports: None,
3117 limit_exceeded: Some(ExecutionLimit::V8Heap),
3118 error: Some(ExecutionErrorBin {
3119 error_type: "Error".into(),
3120 message: "Script execution exceeded the V8 heap limit \
3121 (limits.jsRuntime.v8HeapLimitMb)"
3122 .into(),
3123 stack: String::new(),
3124 code: "ERR_SCRIPT_HEAP_LIMIT_EXCEEDED".into(),
3125 }),
3126 }
3127 } else if cpu_budget_exceeded {
3128 if let Some(budget_ms) = cpu_time_limit_ms {
3129 let capacity = budget_ms as usize;
3130 warn_limit_exhausted(TrackedLimit::V8CpuTimeMs, capacity, capacity);
3131 }
3132 RuntimeEvent::ExecutionResult {
3133 session_id,
3134 exit_code: 1,
3135 exports: None,
3136 limit_exceeded: Some(ExecutionLimit::CpuTime),
3137 error: Some(ExecutionErrorBin {
3138 error_type: "Error".into(),
3139 message: "Script execution exceeded the CPU-time budget \
3140 (limits.jsRuntime.cpuTimeLimitMs)"
3141 .into(),
3142 stack: String::new(),
3143 code: "ERR_SCRIPT_CPU_BUDGET_EXCEEDED".into(),
3144 }),
3145 }
3146 } else if wall_clock_timed_out {
3147 if let Some(limit_ms) = wall_clock_limit_ms {
3148 let capacity = limit_ms as usize;
3149 warn_limit_exhausted(
3150 TrackedLimit::V8WallClockMs,
3151 capacity,
3152 capacity,
3153 );
3154 }
3155 RuntimeEvent::ExecutionResult {
3156 session_id,
3157 exit_code: 1,
3158 exports: None,
3159 limit_exceeded: Some(ExecutionLimit::WallClock),
3160 error: Some(ExecutionErrorBin {
3161 error_type: "Error".into(),
3162 message: "Script execution exceeded the wall-clock limit \
3163 (limits.jsRuntime.wallClockLimitMs)"
3164 .into(),
3165 stack: String::new(),
3166 code: "ERR_SCRIPT_WALL_CLOCK_EXCEEDED".into(),
3167 }),
3168 }
3169 } else if terminated {
3170 RuntimeEvent::ExecutionResult {
3171 session_id,
3172 exit_code: 1,
3173 exports: None,
3174 limit_exceeded: None,
3175 error: Some(ExecutionErrorBin {
3176 error_type: "Error".into(),
3177 message: "Execution terminated".into(),
3178 stack: String::new(),
3179 code: String::new(),
3180 }),
3181 }
3182 } else {
3183 RuntimeEvent::ExecutionResult {
3184 session_id,
3185 exit_code: code,
3186 exports,
3187 limit_exceeded: None,
3188 error: error.map(|e| ExecutionErrorBin {
3189 error_type: e.error_type,
3190 message: e.message,
3191 stack: e.stack,
3192 code: e.code.unwrap_or_default(),
3193 }),
3194 }
3195 };
3196
3197 execution::clear_pending_module_evaluation();
3198 execution::clear_pending_script_evaluation();
3199 execution::clear_module_state();
3200
3201 send_event_with_generation(&event_tx, output_generation, result_frame);
3202 }
3203 #[cfg(test)]
3204 {
3205 let _ = (mode, file_path, bridge_code, post_restore_script, user_code);
3206 }
3207 }
3208 SessionMessage::BridgeResponse(_)
3209 | SessionMessage::StreamEvent(_)
3210 | SessionMessage::TerminateExecution => {
3211 handle_late_session_message(&event_tx, &session_id, output_generation, msg);
3212 }
3213 },
3214 }
3215 }
3216
3217 #[cfg(not(test))]
3219 {
3220 *isolate_handle
3221 .lock()
3222 .expect("session isolate handle lock poisoned") = None;
3223 reset_pending_promises(&mut pending);
3229 drop(_v8_context.take());
3230 isolate::drop_isolate(v8_isolate.take());
3231 }
3232
3233 }
3236
3237struct BridgeFnPartitions {
3240 sync: Vec<&'static str>,
3241 async_fns: Vec<&'static str>,
3242}
3243
3244pub(crate) fn sync_bridge_fns() -> &'static [&'static str] {
3245 &bridge_fn_partitions().sync
3246}
3247
3248pub(crate) fn async_bridge_fns() -> &'static [&'static str] {
3249 &bridge_fn_partitions().async_fns
3250}
3251
3252fn bridge_fn_partitions() -> &'static BridgeFnPartitions {
3253 static PARTITIONS: OnceLock<BridgeFnPartitions> = OnceLock::new();
3254 PARTITIONS.get_or_init(|| BridgeFnPartitions {
3255 sync: bridge_fns_for(|convention| {
3256 matches!(
3257 convention,
3258 BridgeCallConvention::Sync | BridgeCallConvention::SyncPromise
3259 )
3260 }),
3261 async_fns: bridge_fns_for(|convention| convention == BridgeCallConvention::Async),
3262 })
3263}
3264
3265fn bridge_fns_for(filter: impl Fn(BridgeCallConvention) -> bool) -> Vec<&'static str> {
3266 bridge_contract()
3267 .groups
3268 .iter()
3269 .filter(|group| filter(group.convention))
3270 .flat_map(|group| group.names.iter().map(String::as_str))
3271 .collect()
3272}
3273
3274#[doc(hidden)]
3287pub fn reset_pending_promises(pending: &mut crate::bridge::PendingPromises) {
3288 drop(std::mem::take(pending));
3291}
3292
3293#[doc(hidden)]
3305pub fn run_event_loop(
3306 scope: &mut v8::HandleScope,
3307 rx: &Receiver<SessionCommand>,
3308 pending: &crate::bridge::PendingPromises,
3309 abort_rx: Option<&crossbeam_channel::Receiver<()>>,
3310 bridge_rx: Option<&crossbeam_channel::Receiver<BridgeResponse>>,
3311 pause_control: Option<&SessionPauseControl>,
3312) -> EventLoopStatus {
3313 let (ready_broker, ready_rx) = match SessionReadiness::disabled(1) {
3314 Ok(readiness) => readiness,
3315 Err(error) => {
3316 eprintln!("{error}");
3317 return EventLoopStatus::Terminated;
3318 }
3319 };
3320 run_event_loop_with_readiness(
3321 scope,
3322 EventLoopSources {
3323 commands: rx,
3324 readiness: &ready_broker,
3325 readiness_wakes: &ready_rx,
3326 bridge_responses: bridge_rx,
3327 abort: abort_rx,
3328 pause: pause_control,
3329 },
3330 pending,
3331 )
3332}
3333
3334struct EventLoopSources<'a> {
3335 commands: &'a Receiver<SessionCommand>,
3336 readiness: &'a SessionReadiness,
3337 readiness_wakes: &'a Receiver<ReadyWake>,
3338 bridge_responses: Option<&'a Receiver<BridgeResponse>>,
3339 abort: Option<&'a Receiver<()>>,
3340 pause: Option<&'a SessionPauseControl>,
3341}
3342
3343fn run_event_loop_with_readiness(
3344 scope: &mut v8::HandleScope,
3345 sources: EventLoopSources<'_>,
3346 pending: &crate::bridge::PendingPromises,
3347) -> EventLoopStatus {
3348 let EventLoopSources {
3349 commands: rx,
3350 readiness: ready_broker,
3351 readiness_wakes: ready_rx,
3352 bridge_responses: bridge_rx,
3353 abort: abort_rx,
3354 pause: pause_control,
3355 } = sources;
3356 let mut bridge_lane_open = bridge_rx.is_some();
3357 loop {
3358 if abort_rx.is_some_and(execution_abort_requested) {
3363 scope.terminate_execution();
3364 return EventLoopStatus::Terminated;
3365 }
3366 if pending.is_empty()
3367 && !execution::pending_module_evaluation_needs_wait(scope)
3368 && !execution::pending_script_evaluation_needs_wait(scope)
3369 && pending_guest_timer_count(scope) == 0
3370 && pending_guest_immediate_count(scope) == 0
3371 {
3372 break;
3373 }
3374 if let Some(control) = pause_control {
3375 control.wait_while_paused();
3376 }
3377 pump_v8_message_loop(scope);
3378
3379 if bridge_lane_open {
3382 let responses = bridge_rx.expect("open bridge lane must have a receiver");
3383 for _ in 0..64 {
3384 let response = match responses.try_recv() {
3385 Ok(response) => response,
3386 Err(crossbeam_channel::TryRecvError::Empty) => break,
3387 Err(crossbeam_channel::TryRecvError::Disconnected) => {
3388 bridge_lane_open = false;
3389 break;
3390 }
3391 };
3392 let status = dispatch_event_loop_frame(
3393 scope,
3394 SessionMessage::BridgeResponse(response),
3395 pending,
3396 );
3397 if !matches!(status, EventLoopStatus::Completed) {
3398 return status;
3399 }
3400 }
3401 }
3402
3403 scope.perform_microtask_checkpoint();
3409 pump_v8_message_loop(scope);
3410
3411 if pending_guest_immediate_count(scope) > 0 {
3412 match try_recv_session_command(scope, rx, ready_rx, ready_broker, bridge_rx, abort_rx) {
3413 Ok(Some(cmd)) => {
3414 let status = dispatch_session_command(scope, cmd, pending, ready_broker);
3415 if !matches!(status, EventLoopStatus::Completed) {
3416 return status;
3417 }
3418 }
3419 Ok(None) => {
3420 let status = drain_guest_immediates(scope);
3421 if !matches!(status, EventLoopStatus::Completed) {
3422 return status;
3423 }
3424 }
3425 Err(status) => return status,
3426 }
3427 scope.perform_microtask_checkpoint();
3428 pump_v8_message_loop(scope);
3429 }
3430
3431 if pending.is_empty()
3434 && !execution::pending_module_evaluation_needs_wait(scope)
3435 && !execution::pending_script_evaluation_needs_wait(scope)
3436 && pending_guest_timer_count(scope) == 0
3437 && pending_guest_immediate_count(scope) == 0
3438 {
3439 break;
3440 }
3441
3442 let cmd = loop {
3446 if pending_guest_immediate_count(scope) > 0 {
3447 match try_recv_session_command(
3448 scope,
3449 rx,
3450 ready_rx,
3451 ready_broker,
3452 bridge_rx,
3453 abort_rx,
3454 ) {
3455 Ok(Some(cmd)) => break cmd,
3456 Ok(None) => {
3457 let status = drain_guest_immediates(scope);
3458 if !matches!(status, EventLoopStatus::Completed) {
3459 return status;
3460 }
3461 scope.perform_microtask_checkpoint();
3462 pump_v8_message_loop(scope);
3463 continue;
3464 }
3465 Err(status) => return status,
3466 }
3467 }
3468 if bridge_lane_open {
3469 let responses = bridge_rx.expect("open bridge lane must have a receiver");
3470 match responses.try_recv() {
3471 Ok(response) => {
3472 break SessionCommand::Message(SessionMessage::BridgeResponse(response));
3473 }
3474 Err(crossbeam_channel::TryRecvError::Empty) => {}
3475 Err(crossbeam_channel::TryRecvError::Disconnected) => {
3476 bridge_lane_open = false;
3477 }
3478 }
3479 }
3480 match rx.try_recv() {
3485 Ok(command) => break command,
3486 Err(crossbeam_channel::TryRecvError::Empty) => {}
3487 Err(crossbeam_channel::TryRecvError::Disconnected) => {
3488 return EventLoopStatus::Completed;
3489 }
3490 }
3491 if let Ok(wake) = ready_rx.try_recv() {
3492 match ready_batch_command(ready_broker, wake) {
3493 Ok(command) => break command,
3494 Err(error) => {
3495 eprintln!("{error}");
3496 continue;
3497 }
3498 }
3499 }
3500 let mut selector = Select::new();
3505 let ordinary_index = selector.recv(rx);
3506 let ready_index = selector.recv(ready_rx);
3507 let bridge_selection = if bridge_lane_open {
3508 bridge_rx.map(|responses| (selector.recv(responses), responses))
3509 } else {
3510 None
3511 };
3512 let abort_selection = abort_rx.map(|abort| (selector.recv(abort), abort));
3513 let recv_result = match selector.select_timeout(Duration::from_millis(1)) {
3514 Ok(operation) => {
3515 let index = operation.index();
3516 if index == ordinary_index {
3517 operation.recv(rx).ok()
3518 } else if index == ready_index {
3519 match operation.recv(ready_rx) {
3520 Ok(wake) => match ready_batch_command(ready_broker, wake) {
3521 Ok(command) => Some(command),
3522 Err(error) => {
3523 eprintln!("{error}");
3524 None
3525 }
3526 },
3527 Err(_) => None,
3528 }
3529 } else if let Some((bridge_index, responses)) = bridge_selection {
3530 if index == bridge_index {
3531 match operation.recv(responses) {
3532 Ok(response) => Some(SessionCommand::Message(
3533 SessionMessage::BridgeResponse(response),
3534 )),
3535 Err(_) => {
3536 bridge_lane_open = false;
3537 None
3538 }
3539 }
3540 } else if let Some((abort_index, abort)) = abort_selection {
3541 debug_assert_eq!(index, abort_index);
3542 let _ = operation.recv(abort);
3543 scope.terminate_execution();
3544 return EventLoopStatus::Terminated;
3545 } else {
3546 unreachable!("event-loop selector returned an unknown operation")
3547 }
3548 } else if let Some((abort_index, abort)) = abort_selection {
3549 debug_assert_eq!(index, abort_index);
3550 let _ = operation.recv(abort);
3551 scope.terminate_execution();
3552 return EventLoopStatus::Terminated;
3553 } else {
3554 unreachable!("event-loop selector returned an unknown operation")
3555 }
3556 }
3557 Err(_) => None,
3558 };
3559 if let Some(cmd) = recv_result {
3560 break cmd;
3561 }
3562 if let Some(control) = pause_control {
3563 control.wait_while_paused();
3564 }
3565 scope.perform_microtask_checkpoint();
3568 pump_v8_message_loop(scope);
3569 if pending.is_empty()
3571 && !execution::pending_module_evaluation_needs_wait(scope)
3572 && !execution::pending_script_evaluation_needs_wait(scope)
3573 && pending_guest_timer_count(scope) == 0
3574 && pending_guest_immediate_count(scope) == 0
3575 {
3576 return EventLoopStatus::Completed;
3577 }
3578 };
3579
3580 let status = dispatch_session_command(scope, cmd, pending, ready_broker);
3581 if !matches!(status, EventLoopStatus::Completed) {
3582 return status;
3583 }
3584 }
3585 EventLoopStatus::Completed
3586}
3587
3588fn execution_abort_requested(abort: &crossbeam_channel::Receiver<()>) -> bool {
3589 !matches!(
3590 abort.try_recv(),
3591 Err(crossbeam_channel::TryRecvError::Empty)
3592 )
3593}
3594
3595fn try_recv_session_command(
3596 scope: &mut v8::HandleScope,
3597 rx: &Receiver<SessionCommand>,
3598 ready_rx: &Receiver<ReadyWake>,
3599 ready_broker: &SessionReadiness,
3600 bridge_rx: Option<&crossbeam_channel::Receiver<BridgeResponse>>,
3601 abort_rx: Option<&crossbeam_channel::Receiver<()>>,
3602) -> Result<Option<SessionCommand>, EventLoopStatus> {
3603 if let Some(responses) = bridge_rx {
3604 match responses.try_recv() {
3605 Ok(response) => {
3606 return Ok(Some(SessionCommand::Message(
3607 SessionMessage::BridgeResponse(response),
3608 )));
3609 }
3610 Err(crossbeam_channel::TryRecvError::Empty) => {}
3611 Err(crossbeam_channel::TryRecvError::Disconnected) => {}
3612 }
3613 }
3614 if let Some(abort) = abort_rx {
3615 match abort.try_recv() {
3616 Ok(()) | Err(crossbeam_channel::TryRecvError::Disconnected) => {
3617 scope.terminate_execution();
3618 return Err(EventLoopStatus::Terminated);
3619 }
3620 Err(crossbeam_channel::TryRecvError::Empty) => {}
3621 }
3622 }
3623 match rx.try_recv() {
3624 Ok(command) => return Ok(Some(command)),
3625 Err(crossbeam_channel::TryRecvError::Empty) => {}
3626 Err(crossbeam_channel::TryRecvError::Disconnected) => return Ok(None),
3627 }
3628 match ready_rx.try_recv() {
3629 Ok(wake) => match ready_batch_command(ready_broker, wake) {
3630 Ok(command) => return Ok(Some(command)),
3631 Err(error) => {
3632 eprintln!("{error}");
3633 }
3634 },
3635 Err(crossbeam_channel::TryRecvError::Empty) => {}
3636 Err(crossbeam_channel::TryRecvError::Disconnected) => {}
3637 }
3638 if let Some(abort) = abort_rx {
3639 crossbeam_channel::select! {
3640 recv(abort) -> _ => {
3641 scope.terminate_execution();
3642 Err(EventLoopStatus::Terminated)
3643 },
3644 recv(rx) -> result => Ok(result.ok()),
3645 default => Ok(None),
3646 }
3647 } else {
3648 match rx.try_recv() {
3649 Ok(cmd) => Ok(Some(cmd)),
3650 Err(crossbeam_channel::TryRecvError::Empty) => Ok(None),
3651 Err(crossbeam_channel::TryRecvError::Disconnected) => Ok(None),
3652 }
3653 }
3654}
3655
3656fn dispatch_session_command(
3657 scope: &mut v8::HandleScope,
3658 cmd: SessionCommand,
3659 pending: &crate::bridge::PendingPromises,
3660 ready_broker: &SessionReadiness,
3661) -> EventLoopStatus {
3662 match cmd {
3663 SessionCommand::Message(frame) => dispatch_event_loop_frame(scope, frame, pending),
3664 SessionCommand::ReadyBatch(batch) => dispatch_ready_batch(scope, batch, ready_broker),
3665 SessionCommand::SetModuleReader(reader) => {
3666 execution::install_session_guest_reader(Some(reader));
3667 EventLoopStatus::Completed
3668 }
3669 SessionCommand::Shutdown => EventLoopStatus::Terminated,
3670 }
3671}
3672
3673fn dispatch_ready_batch(
3681 scope: &mut v8::HandleScope,
3682 batch: RuntimeReadyBatch,
3683 ready_broker: &SessionReadiness,
3684) -> EventLoopStatus {
3685 let mut delivered = Vec::with_capacity(batch.entries.len());
3686 let dispatch_status =
3687 dispatch_ready_batch_callbacks(scope, &batch, ready_broker, &mut delivered);
3688 complete_ready_batch_dispatch(ready_broker, &batch, &delivered, dispatch_status)
3689}
3690
3691fn dispatch_ready_batch_callbacks(
3692 scope: &mut v8::HandleScope,
3693 batch: &RuntimeReadyBatch,
3694 ready_broker: &SessionReadiness,
3695 delivered: &mut Vec<ReadyObservation>,
3696) -> EventLoopStatus {
3697 for entry in &batch.entries {
3698 let tc = &mut v8::TryCatch::new(scope);
3699 let dispatch = crate::stream::dispatch_readiness(
3700 tc,
3701 entry.capability_id,
3702 entry.capability_generation,
3703 entry.flags,
3704 );
3705 tc.perform_microtask_checkpoint();
3706 if let Some(exception) = tc.exception() {
3707 let (code, error) = execution::exception_to_result(tc, exception);
3708 return EventLoopStatus::Failed(code, error);
3709 }
3710 match dispatch {
3711 crate::stream::ReadinessDispatch::Delivered => delivered.push(*entry),
3712 crate::stream::ReadinessDispatch::TargetMissing => {
3713 }
3718 crate::stream::ReadinessDispatch::BridgeMissing => {
3719 return EventLoopStatus::Failed(
3720 1,
3721 ExecutionError {
3722 error_type: String::from("Error"),
3723 message: String::from(
3724 "ERR_AGENTOS_READY_DISPATCH_MISSING: guest bridge does not expose _agentOSReadyDispatch",
3725 ),
3726 stack: String::new(),
3727 code: Some(String::from("ERR_AGENTOS_READY_DISPATCH_MISSING")),
3728 },
3729 );
3730 }
3731 }
3732 }
3733 if batch.signals_ready {
3734 let signals = match ready_broker.drain_signals(batch) {
3735 Ok(signals) => signals,
3736 Err(error) => return readiness_dispatch_failure(error),
3737 };
3738 for signal in signals {
3739 let Some(signal_name) = signal_name_for_stream_event(signal) else {
3740 continue;
3741 };
3742 let tc = &mut v8::TryCatch::new(scope);
3743 crate::stream::dispatch_signal_event(tc, signal_name, signal);
3744 tc.perform_microtask_checkpoint();
3745 if let Some(exception) = tc.exception() {
3746 let (code, error) = execution::exception_to_result(tc, exception);
3747 return EventLoopStatus::Failed(code, error);
3748 }
3749 if let Some(error) = execution::take_unhandled_promise_rejection(tc) {
3750 return EventLoopStatus::Failed(1, error);
3751 }
3752 }
3753 }
3754 if batch.timers_ready {
3755 let timers = match ready_broker.drain_timers(batch) {
3756 Ok(timers) => timers,
3757 Err(error) => return readiness_dispatch_failure(error),
3758 };
3759 for timer_id in timers {
3760 let tc = &mut v8::TryCatch::new(scope);
3761 crate::stream::dispatch_timer_event(tc, timer_id);
3762 tc.perform_microtask_checkpoint();
3763 if let Some(exception) = tc.exception() {
3764 let (code, error) = execution::exception_to_result(tc, exception);
3765 return EventLoopStatus::Failed(code, error);
3766 }
3767 if let Some(error) = execution::take_unhandled_promise_rejection(tc) {
3768 return EventLoopStatus::Failed(1, error);
3769 }
3770 }
3771 }
3772 EventLoopStatus::Completed
3773}
3774
3775fn complete_ready_batch_dispatch(
3776 ready_broker: &SessionReadiness,
3777 batch: &RuntimeReadyBatch,
3778 delivered: &[ReadyObservation],
3779 dispatch_status: EventLoopStatus,
3780) -> EventLoopStatus {
3781 if let Err(error) = ready_broker.complete_batch(batch, delivered) {
3782 if matches!(&dispatch_status, EventLoopStatus::Completed) {
3783 return readiness_dispatch_failure(error);
3784 }
3785 eprintln!(
3786 "ERR_AGENTOS_READY_COMPLETE_AFTER_DISPATCH_FAILURE: could not complete readiness wake after guest dispatch failed: {error}"
3787 );
3788 }
3789 dispatch_status
3790}
3791
3792fn readiness_dispatch_failure(message: String) -> EventLoopStatus {
3793 EventLoopStatus::Failed(
3794 1,
3795 ExecutionError {
3796 error_type: String::from("Error"),
3797 message,
3798 stack: String::new(),
3799 code: Some(String::from("ERR_AGENTOS_READY_COMPLETE")),
3800 },
3801 )
3802}
3803
3804fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
3805 match signal {
3806 1 => Some("SIGHUP"),
3807 2 => Some("SIGINT"),
3808 10 => Some("SIGUSR1"),
3809 14 => Some("SIGALRM"),
3810 18 => Some("SIGCONT"),
3811 15 => Some("SIGTERM"),
3812 17 => Some("SIGCHLD"),
3813 28 => Some("SIGWINCH"),
3814 _ => None,
3815 }
3816}
3817
3818fn pending_guest_timer_count(scope: &mut v8::HandleScope) -> usize {
3819 let tc = &mut v8::TryCatch::new(scope);
3820 let context = tc.get_current_context();
3821 let global = context.global(tc);
3822 let key = match v8::String::new(tc, "_getPendingTimerCount") {
3823 Some(key) => key,
3824 None => return 0,
3825 };
3826 let Some(func_value) = global.get(tc, key.into()) else {
3827 return 0;
3828 };
3829 let Ok(func) = v8::Local::<v8::Function>::try_from(func_value) else {
3830 return 0;
3831 };
3832 let Some(result) = func.call(tc, global.into(), &[]) else {
3833 return 0;
3834 };
3835
3836 result
3837 .integer_value(tc)
3838 .and_then(|count| usize::try_from(count).ok())
3839 .unwrap_or(0)
3840}
3841
3842fn pending_guest_immediate_count(scope: &mut v8::HandleScope) -> usize {
3843 let tc = &mut v8::TryCatch::new(scope);
3844 let context = tc.get_current_context();
3845 let global = context.global(tc);
3846 let key = match v8::String::new(tc, "_getPendingImmediateCount") {
3847 Some(key) => key,
3848 None => return 0,
3849 };
3850 let Some(func_value) = global.get(tc, key.into()) else {
3851 return 0;
3852 };
3853 let Ok(func) = v8::Local::<v8::Function>::try_from(func_value) else {
3854 return 0;
3855 };
3856 let Some(result) = func.call(tc, global.into(), &[]) else {
3857 return 0;
3858 };
3859
3860 result
3861 .integer_value(tc)
3862 .and_then(|count| usize::try_from(count).ok())
3863 .unwrap_or(0)
3864}
3865
3866fn drain_guest_immediates(scope: &mut v8::HandleScope) -> EventLoopStatus {
3867 let tc = &mut v8::TryCatch::new(scope);
3868 let context = tc.get_current_context();
3869 let global = context.global(tc);
3870 let key = match v8::String::new(tc, "_drainImmediates") {
3871 Some(key) => key,
3872 None => return EventLoopStatus::Completed,
3873 };
3874 let Some(func_value) = global.get(tc, key.into()) else {
3875 return EventLoopStatus::Completed;
3876 };
3877 let Ok(func) = v8::Local::<v8::Function>::try_from(func_value) else {
3878 return EventLoopStatus::Completed;
3879 };
3880 let _ = func.call(tc, global.into(), &[]);
3881 tc.perform_microtask_checkpoint();
3882 if let Some(exception) = tc.exception() {
3883 let (code, err) = execution::exception_to_result(tc, exception);
3884 return EventLoopStatus::Failed(code, err);
3885 }
3886 if let Some(err) = execution::take_unhandled_promise_rejection(tc) {
3887 return EventLoopStatus::Failed(1, err);
3888 }
3889 EventLoopStatus::Completed
3890}
3891
3892fn pump_v8_message_loop(scope: &mut v8::HandleScope) {
3893 let platform = v8::V8::get_current_platform();
3894 while v8::Platform::pump_message_loop(&platform, scope, false) {
3895 scope.perform_microtask_checkpoint();
3896 }
3897}
3898
3899#[derive(Debug)]
3902#[doc(hidden)]
3903pub enum EventLoopStatus {
3904 Completed,
3905 Terminated,
3906 Failed(i32, ExecutionError),
3907}
3908
3909fn dispatch_event_loop_frame(
3910 scope: &mut v8::HandleScope,
3911 frame: SessionMessage,
3912 pending: &crate::bridge::PendingPromises,
3913) -> EventLoopStatus {
3914 match frame {
3915 SessionMessage::BridgeResponse(BridgeResponse {
3916 call_id,
3917 status,
3918 payload,
3919 reservation: _reservation,
3920 }) => {
3921 let (result, error) = if status == 1 {
3922 (None, Some(String::from_utf8_lossy(&payload).to_string()))
3923 } else if status == 2 || !payload.is_empty() {
3924 (Some(payload), None)
3926 } else {
3927 (None, None)
3928 };
3929 let _ = crate::bridge::resolve_pending_promise(
3930 scope, pending, call_id, status, result, error,
3931 );
3932 EventLoopStatus::Completed
3934 }
3935 SessionMessage::StreamEvent(StreamEvent {
3936 event_type,
3937 payload,
3938 }) => {
3939 let tc = &mut v8::TryCatch::new(scope);
3940 crate::stream::dispatch_stream_event(tc, &event_type, &payload);
3941 tc.perform_microtask_checkpoint();
3942 if let Some(exception) = tc.exception() {
3943 let (code, err) = execution::exception_to_result(tc, exception);
3944 return EventLoopStatus::Failed(code, err);
3945 }
3946 if let Some(err) = execution::take_unhandled_promise_rejection(tc) {
3947 return EventLoopStatus::Failed(1, err);
3948 }
3949 EventLoopStatus::Completed
3950 }
3951 SessionMessage::TerminateExecution => {
3952 scope.terminate_execution();
3953 EventLoopStatus::Terminated
3954 }
3955 _ => {
3956 EventLoopStatus::Completed
3958 }
3959 }
3960}
3961
3962#[cfg(test)]
3963mod tests {
3964 use super::*;
3965 use std::collections::HashSet;
3966
3967 const TEST_READY_BATCH_HANDLES: usize = 64;
3968
3969 fn test_manager(max: usize) -> SessionManager {
3971 test_manager_with_events(max).0
3972 }
3973
3974 fn test_manager_with_events(max: usize) -> (SessionManager, Receiver<RuntimeEventEnvelope>) {
3975 let (tx, _rx) = crossbeam_channel::unbounded();
3976 let router: CallIdRouter = Arc::new(BridgeCallRegistry::with_default_limit());
3977 let snap_cache = Arc::new(SnapshotCache::new(4));
3978 let runtime =
3979 agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default())
3980 .expect("create test process runtime")
3981 .context();
3982 let manager = SessionManager::new(Some(max), tx, router, snap_cache, runtime);
3983 (manager, _rx)
3984 }
3985
3986 #[test]
3987 fn zero_cpu_time_limit_is_normalized_to_no_timeout() {
3988 assert_eq!(normalize_cpu_time_limit_ms(None), None);
3989 assert_eq!(normalize_cpu_time_limit_ms(Some(0)), None);
3990 assert_eq!(normalize_cpu_time_limit_ms(Some(1)), Some(1));
3991 }
3992
3993 #[test]
3994 fn vm_executor_permits_report_active_and_high_water_metrics() {
3995 let control: SlotControl = Arc::new((Mutex::new(0), Condvar::new()));
3996 let metrics = RuntimeMetrics::new();
3997
3998 let first = SessionSlotPermit::try_acquire(&control, Some(2), metrics.clone())
3999 .expect("acquire first VM executor");
4000 let second = SessionSlotPermit::try_acquire(&control, Some(2), metrics.clone())
4001 .expect("acquire second VM executor");
4002 let active = metrics.snapshot().executors[ExecutorMetricClass::Vm.index()].active;
4003 assert_eq!(active.current, 2);
4004 assert_eq!(active.high_water, 2);
4005
4006 drop(first);
4007 assert_eq!(
4008 metrics.snapshot().executors[ExecutorMetricClass::Vm.index()]
4009 .active
4010 .current,
4011 1
4012 );
4013
4014 drop(second);
4015 let released = metrics.snapshot().executors[ExecutorMetricClass::Vm.index()].active;
4016 assert_eq!(released.current, 0);
4017 assert_eq!(released.high_water, 2);
4018 }
4019
4020 #[test]
4021 fn configured_executor_and_command_bounds_drive_session_manager() {
4022 const SUBPROCESS_ENV: &str = "AGENTOS_V8_CONFIGURED_SESSION_MANAGER_SUBPROCESS";
4023 if std::env::var_os(SUBPROCESS_ENV).is_none() {
4024 let test_name =
4025 "session::tests::configured_executor_and_command_bounds_drive_session_manager";
4026 let output =
4027 std::process::Command::new(std::env::current_exe().expect("current test binary"))
4028 .arg(test_name)
4029 .arg("--exact")
4030 .arg("--nocapture")
4031 .env(SUBPROCESS_ENV, "1")
4032 .output()
4033 .unwrap_or_else(|error| panic!("spawn isolated test {test_name}: {error}"));
4034 assert!(
4035 output.status.success(),
4036 "isolated test {test_name} failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
4037 output.status.code(),
4038 String::from_utf8_lossy(&output.stdout),
4039 String::from_utf8_lossy(&output.stderr),
4040 );
4041 return;
4042 }
4043 let mut config = agentos_runtime::RuntimeConfig {
4044 max_active_vm_executors: Some(3),
4045 vm_executor_teardown_timeout_ms: 23,
4046 ..agentos_runtime::RuntimeConfig::default()
4047 };
4048 config.resources.max_handle_commands = 7;
4049 let runtime = agentos_runtime::SidecarRuntime::process(&config)
4050 .expect("configured process runtime")
4051 .context();
4052 let (event_tx, _event_rx) = crossbeam_channel::unbounded();
4053 let router: CallIdRouter = Arc::new(BridgeCallRegistry::with_default_limit());
4054 let mut manager = SessionManager::new(
4055 runtime.max_active_vm_executors(),
4056 event_tx,
4057 router,
4058 Arc::new(SnapshotCache::new(1)),
4059 runtime,
4060 );
4061
4062 assert_eq!(manager.max_concurrency, Some(3));
4063 assert_eq!(manager.executor_teardown_timeout, Duration::from_millis(23));
4064 manager
4065 .create_session("configured-bounds".into(), None, None, None)
4066 .expect("create bounded session");
4067 assert_eq!(manager.sessions["configured-bounds"].command_capacity, 7);
4068 manager
4069 .destroy_session("configured-bounds")
4070 .expect("destroy bounded session");
4071 }
4072
4073 fn expect_late_message_warning(
4074 rx: &Receiver<RuntimeEventEnvelope>,
4075 session_id: &str,
4076 error_code: &str,
4077 detail_fragment: &str,
4078 ) {
4079 let event = rx
4080 .recv_timeout(std::time::Duration::from_millis(200))
4081 .expect("late-message warning");
4082 match event.event {
4083 RuntimeEvent::Log {
4084 session_id: observed_session_id,
4085 channel,
4086 message,
4087 } => {
4088 assert_eq!(observed_session_id, session_id);
4089 assert_eq!(channel, 1, "late warnings should use stderr channel");
4090 assert!(
4091 message.contains(error_code),
4092 "warning should contain error code {error_code}, got {message}"
4093 );
4094 assert!(
4095 message.contains(detail_fragment),
4096 "warning should mention {detail_fragment}, got {message}"
4097 );
4098 }
4099 other => panic!("expected late-message warning log, got {other:?}"),
4100 }
4101 }
4102
4103 #[test]
4104 fn bridge_contract_function_partitions_cover_contract() {
4105 let contract = bridge_contract();
4106
4107 let expected_sync = contract
4108 .groups
4109 .iter()
4110 .filter(|group| {
4111 matches!(
4112 group.convention,
4113 BridgeCallConvention::Sync | BridgeCallConvention::SyncPromise
4114 )
4115 })
4116 .flat_map(|group| group.names.iter().map(String::as_str))
4117 .collect::<HashSet<_>>();
4118 let expected_async = contract
4119 .groups
4120 .iter()
4121 .filter(|group| group.convention == BridgeCallConvention::Async)
4122 .flat_map(|group| group.names.iter().map(String::as_str))
4123 .collect::<HashSet<_>>();
4124
4125 let sync_names = sync_bridge_fns();
4126 let async_names = async_bridge_fns();
4127 let registered_sync = sync_names.iter().copied().collect::<HashSet<_>>();
4128 let registered_async = async_names.iter().copied().collect::<HashSet<_>>();
4129
4130 assert_eq!(
4131 registered_sync, expected_sync,
4132 "sync bridge function partition drifted from crates/bridge/bridge-contract.json"
4133 );
4134 assert_eq!(
4135 registered_async, expected_async,
4136 "async bridge function partition drifted from crates/bridge/bridge-contract.json"
4137 );
4138 assert!(
4139 registered_sync.is_disjoint(®istered_async),
4140 "sync and async bridge function partitions must not overlap"
4141 );
4142 }
4143
4144 #[test]
4145 fn session_management() {
4146 {
4151 let mut mgr = test_manager(4);
4152
4153 mgr.create_session("session-aaa".into(), None, None, None)
4154 .expect("create session A");
4155 assert_eq!(mgr.session_count(), 1);
4156
4157 std::thread::sleep(std::time::Duration::from_millis(200));
4159
4160 mgr.destroy_session("session-aaa")
4162 .expect("destroy session A");
4163 assert_eq!(mgr.session_count(), 0);
4164 }
4165
4166 {
4168 let mut mgr = test_manager(4);
4169
4170 mgr.create_session("session-bbb".into(), None, None, None)
4171 .expect("create session B");
4172 mgr.create_session("session-ccc".into(), Some(16), None, None)
4173 .expect("create session C");
4174 assert_eq!(mgr.session_count(), 2);
4175
4176 std::thread::sleep(std::time::Duration::from_millis(200));
4177
4178 let err = mgr.create_session("session-bbb".into(), None, None, None);
4180 assert!(err.is_err());
4181 assert!(err.unwrap_err().contains("already exists"));
4182
4183 let err = mgr.send_to_session("missing", SessionMessage::TerminateExecution);
4185 assert!(err.is_err());
4186 assert!(err.unwrap_err().contains("does not exist"));
4187
4188 let err = mgr.destroy_session("no-such-session");
4190 assert!(err.is_err());
4191 assert!(err.unwrap_err().contains("does not exist"));
4192
4193 mgr.destroy_sessions(["session-bbb".into(), "session-ccc".into()]);
4194 assert_eq!(mgr.session_count(), 0);
4195 }
4196
4197 {
4199 let mut mgr = test_manager(2);
4200
4201 mgr.create_session("s1".into(), None, None, None)
4202 .expect("create s1");
4203 mgr.create_session("s2".into(), None, None, None)
4204 .expect("create s2");
4205 let error = mgr
4206 .create_session("s3".into(), None, None, None)
4207 .expect_err("third executor must be rejected before thread creation");
4208 assert!(error.contains("ERR_AGENTOS_VM_EXECUTOR_LIMIT"));
4209
4210 std::thread::sleep(std::time::Duration::from_millis(300));
4212
4213 assert_eq!(mgr.active_slot_count(), 2);
4215 assert_eq!(mgr.session_count(), 2);
4216
4217 mgr.destroy_session("s1").expect("destroy s1");
4219 mgr.create_session("s3".into(), None, None, None)
4220 .expect("create s3 after release");
4221 std::thread::sleep(std::time::Duration::from_millis(300));
4222 assert_eq!(mgr.active_slot_count(), 2);
4223 assert_eq!(mgr.session_count(), 2);
4224
4225 mgr.destroy_sessions(["s2".into(), "s3".into()]);
4227 std::thread::sleep(std::time::Duration::from_millis(100));
4228 assert_eq!(mgr.session_count(), 0);
4229 assert_eq!(mgr.active_slot_count(), 0);
4230 }
4231 }
4232
4233 #[test]
4234 fn detach_session_clears_call_id_routes_for_session() {
4235 let mut mgr = test_manager(1);
4236 mgr.create_session_with_output_generation(
4237 "session-route".into(),
4238 None,
4239 None,
4240 None,
4241 Some(7),
4242 None,
4243 )
4244 .expect("create session");
4245 let _waiter = mgr
4246 .call_id_router()
4247 .register_sync(&mgr.runtime, 0, 1, 42, "session-route", Some(7))
4248 .expect("register bridge call target");
4249
4250 assert!(
4251 mgr.detach_session_if_output_generation("session-route", 7)
4252 .expect("detach session"),
4253 "matching output generation should detach session"
4254 );
4255 assert!(
4256 mgr.call_id_router().pending_len() == 0,
4257 "detach should clear stale bridge call routes for the session"
4258 );
4259 assert_eq!(
4260 mgr.quarantined.len(),
4261 1,
4262 "detached executor join ownership must remain in the manager"
4263 );
4264 for handle in mgr.take_session_shutdown_handles() {
4265 handle.join().expect("join quarantined executor");
4266 }
4267 assert_eq!(mgr.active_slot_count(), 0);
4268 }
4269
4270 #[test]
4271 fn begin_destroy_session_removes_entry_before_finish() {
4272 let mut mgr = test_manager(1);
4273 mgr.create_session("two-phase".into(), None, None, None)
4274 .expect("create session");
4275
4276 let first_shutdown = mgr
4277 .begin_destroy_session("two-phase")
4278 .expect("begin destroy session");
4279 assert_eq!(
4280 mgr.session_count(),
4281 0,
4282 "entry should be removed before the shutdown is finished"
4283 );
4284
4285 let error = mgr
4288 .create_session("two-phase".into(), None, None, None)
4289 .expect_err("old generation must retain its executor permit");
4290 assert!(error.contains("ERR_AGENTOS_VM_EXECUTOR_LIMIT"));
4291 first_shutdown.finish();
4292
4293 mgr.create_session("two-phase".into(), None, None, None)
4294 .expect("re-create session after old generation joins");
4295 let second_shutdown = mgr
4296 .begin_destroy_session("two-phase")
4297 .expect("begin destroy re-created session");
4298 second_shutdown.finish();
4299 assert_eq!(mgr.session_count(), 0);
4300 }
4301
4302 #[test]
4303 fn shutdown_bypasses_a_full_ordinary_command_lane() {
4304 let mut mgr = test_manager(1);
4305 let command_capacity = mgr
4306 .runtime
4307 .resources()
4308 .usage(ResourceClass::HandleCommands)
4309 .limit
4310 .expect("configured command capacity");
4311 let (tx, rx) = crossbeam_channel::bounded(command_capacity);
4312 for index in 0..command_capacity {
4313 tx.send(SessionCommand::Message(SessionMessage::StreamEvent(
4314 StreamEvent {
4315 event_type: format!("ordinary-{index}"),
4316 payload: Vec::new(),
4317 },
4318 )))
4319 .expect("fill ordinary command lane");
4320 }
4321 let (shutdown_tx, shutdown_rx) = crossbeam_channel::bounded(1);
4322 let join_handle = thread::spawn(move || {
4323 shutdown_rx.recv().expect("dedicated shutdown token");
4324 drop(rx);
4325 });
4326 let (ready_broker, _ready_rx) =
4327 SessionReadiness::new(1, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4328 .expect("create session readiness");
4329 let session_resources = Arc::clone(mgr.runtime.resources());
4330 mgr.sessions.insert(
4331 String::from("full-command-lane"),
4332 SessionEntry {
4333 output_generation: None,
4334 tx,
4335 command_capacity,
4336 shutdown_tx,
4337 join_handle: Some(join_handle),
4338 isolate_handle: Arc::new(Mutex::new(None)),
4339 execution_abort: new_execution_abort(),
4340 execution_active: Arc::new(AtomicBool::new(false)),
4341 pause_control: Arc::new(SessionPauseControl::default()),
4342 ready_broker,
4343 session_resources,
4344 },
4345 );
4346
4347 mgr.begin_destroy_session("full-command-lane")
4348 .expect("begin destroy overloaded session")
4349 .finish();
4350 assert_eq!(mgr.session_count(), 0);
4351 }
4352
4353 #[test]
4354 fn execution_abort_is_durable_when_signaled_before_waiter_arm() {
4355 let execution_abort = new_execution_abort();
4356 signal_execution_abort_durable(&execution_abort, ExecutionAbortReason::Terminated);
4357
4358 let (_guard, receiver) = ActiveExecutionAbort::arm(&execution_abort);
4359 assert_eq!(
4360 receiver.recv_timeout(Duration::from_millis(10)),
4361 Err(crossbeam_channel::RecvTimeoutError::Disconnected),
4362 "a waiter armed after termination must observe it immediately"
4363 );
4364 }
4365
4366 #[test]
4367 fn session_shutdown_finish_clears_late_call_routes() {
4368 let mut mgr = test_manager(1);
4369 mgr.create_session("late-route".into(), None, None, None)
4370 .expect("create session");
4371
4372 let shutdown = mgr
4373 .begin_destroy_session("late-route")
4374 .expect("begin destroy session");
4375 let _waiter = mgr
4378 .call_id_router()
4379 .register_sync(&mgr.runtime, 0, 1, 42, "late-route", None)
4380 .expect("register late bridge call target");
4381
4382 shutdown.finish();
4383 assert!(
4384 mgr.call_id_router().pending_len() == 0,
4385 "finish should clear call routes registered during shutdown"
4386 );
4387 }
4388
4389 #[test]
4390 fn late_terminate_execution_is_logged_instead_of_silently_dropped() {
4391 let (mut mgr, rx) = test_manager_with_events(1);
4392 mgr.create_session("late-terminate".into(), None, None, None)
4393 .expect("create session");
4394
4395 mgr.send_to_session("late-terminate", SessionMessage::TerminateExecution)
4396 .expect("send late terminate");
4397
4398 expect_late_message_warning(
4399 &rx,
4400 "late-terminate",
4401 LATE_TERMINATE_EXECUTION_ERROR_CODE,
4402 "TerminateExecution",
4403 );
4404
4405 mgr.destroy_session("late-terminate")
4406 .expect("destroy session");
4407 }
4408
4409 #[test]
4410 fn late_stdin_end_is_an_expected_stale_control_event() {
4411 let (event_tx, event_rx) = crossbeam_channel::unbounded();
4412 handle_late_session_message(
4413 &RuntimeEventSender::from(event_tx),
4414 "completed-session",
4415 Some(7),
4416 SessionMessage::StreamEvent(StreamEvent {
4417 event_type: String::from("stdin_end"),
4418 payload: Vec::new(),
4419 }),
4420 );
4421
4422 assert!(
4423 event_rx.try_recv().is_err(),
4424 "an idempotent stdin EOF racing process exit must not become guest stderr"
4425 );
4426 }
4427
4428 #[test]
4429 fn late_bridge_response_is_logged_instead_of_silently_dropped() {
4430 let (mut mgr, rx) = test_manager_with_events(1);
4431 mgr.create_session("late-bridge".into(), None, None, None)
4432 .expect("create session");
4433
4434 mgr.send_to_session(
4435 "late-bridge",
4436 SessionMessage::BridgeResponse(BridgeResponse {
4437 call_id: 41,
4438 status: 0,
4439 payload: vec![0xAA, 0xBB],
4440 reservation: None,
4441 }),
4442 )
4443 .expect("send late bridge response");
4444
4445 expect_late_message_warning(
4446 &rx,
4447 "late-bridge",
4448 LATE_BRIDGE_RESPONSE_ERROR_CODE,
4449 "BridgeResponse",
4450 );
4451
4452 mgr.destroy_session("late-bridge").expect("destroy session");
4453 }
4454
4455 #[test]
4456 fn control_only_readiness_wake_becomes_a_command_and_rearms() {
4457 let mgr = test_manager(1);
4458 let (broker, wake_rx) = SessionReadiness::new(23, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4459 .expect("create session readiness");
4460
4461 broker.publish_timer(91).expect("publish first timer");
4462 let first_wake = wake_rx.try_recv().expect("first timer wake");
4463 let first_batch = match ready_batch_command(&broker, first_wake).expect("timer command") {
4464 SessionCommand::ReadyBatch(batch) => batch,
4465 _ => panic!("timer wake must produce a readiness command"),
4466 };
4467 assert!(first_batch.entries.is_empty());
4468 assert!(first_batch.timers_ready);
4469 assert_eq!(
4470 broker
4471 .drain_timers(&first_batch)
4472 .expect("drain first timer"),
4473 vec![91]
4474 );
4475 broker
4476 .complete_batch(&first_batch, &[])
4477 .expect("complete first timer wake");
4478
4479 broker.publish_timer(92).expect("publish second timer");
4480 let second_wake = wake_rx
4481 .try_recv()
4482 .expect("completing a control-only batch must rearm the wake lane");
4483 let second_batch = match ready_batch_command(&broker, second_wake).expect("second command")
4484 {
4485 SessionCommand::ReadyBatch(batch) => batch,
4486 _ => panic!("second timer wake must produce a readiness command"),
4487 };
4488 assert!(second_batch.entries.is_empty());
4489 assert!(second_batch.timers_ready);
4490 }
4491
4492 #[test]
4493 fn admitted_session_command_precedes_later_signal_wake() {
4494 let mgr = test_manager(1);
4495 let (broker, ready_rx) = SessionReadiness::new(31, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4496 .expect("create session readiness");
4497 let (tx, rx) = crossbeam_channel::bounded(2);
4498 let (_shutdown_tx, shutdown_rx) = crossbeam_channel::bounded(1);
4499
4500 tx.send(SessionCommand::Message(SessionMessage::StreamEvent(
4501 StreamEvent {
4502 event_type: String::from("execute-admitted-first"),
4503 payload: Vec::new(),
4504 },
4505 )))
4506 .expect("queue ordinary session command");
4507 broker.publish_signal(15).expect("publish later SIGTERM");
4508
4509 assert!(matches!(
4510 recv_session_command(&rx, &shutdown_rx, &ready_rx, &broker),
4511 Some(SessionCommand::Message(SessionMessage::StreamEvent(_)))
4512 ));
4513 let Some(SessionCommand::ReadyBatch(batch)) =
4514 recv_session_command(&rx, &shutdown_rx, &ready_rx, &broker)
4515 else {
4516 panic!("later signal wake must remain queued after the admitted command");
4517 };
4518 assert!(batch.signals_ready);
4519 assert_eq!(
4520 broker.drain_signals(&batch).expect("drain signal"),
4521 vec![15]
4522 );
4523 broker
4524 .complete_batch(&batch, &[])
4525 .expect("complete signal wake");
4526 }
4527
4528 #[test]
4529 fn readiness_flood_uses_one_wake_and_carries_only_capability_identity() {
4530 let mgr = test_manager(1);
4531 let (broker, wake_rx) = SessionReadiness::new(7, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4532 .expect("create session readiness");
4533 for _ in 0..1_000_000 {
4534 broker
4535 .publish(41, 3, ReadyFlags::READABLE)
4536 .expect("publish readiness");
4537 }
4538
4539 let wake = wake_rx.try_recv().expect("one coalesced wake");
4540 assert!(
4541 wake_rx.try_recv().is_err(),
4542 "wake lane must have capacity one"
4543 );
4544 let batch = broker.take_batch(wake).expect("take readiness batch");
4545 assert_eq!(batch.entries.len(), 1);
4546 assert_eq!(batch.entries[0].capability_id, 41);
4547 assert_eq!(batch.entries[0].capability_generation, 3);
4548 assert_eq!(batch.entries[0].flags, ReadyFlags::READABLE);
4549 assert_eq!(batch.entries[0].revision, 1_000_000);
4550 broker
4551 .complete_batch(&batch, &batch.entries)
4552 .expect("complete readiness");
4553 assert_eq!(
4554 broker.broker.pending_handle_count().expect("pending count"),
4555 0
4556 );
4557 }
4558
4559 #[test]
4560 fn readiness_batch_honors_vm_reactor_work_quantum_override() {
4561 let mgr = test_manager(1);
4562 let (broker, wake_rx) =
4563 SessionReadiness::new(29, &mgr.runtime, 2).expect("create bounded readiness");
4564 for capability_id in 1..=3 {
4565 broker
4566 .publish(capability_id, 1, ReadyFlags::READABLE)
4567 .expect("publish readiness");
4568 }
4569
4570 let first_wake = wake_rx.recv().expect("first coalesced wake");
4571 let first_batch = broker.take_batch(first_wake).expect("first bounded batch");
4572 assert_eq!(
4573 first_batch.entries.len(),
4574 2,
4575 "limits.reactor.workQuantum must cap one V8 readiness turn"
4576 );
4577 broker
4578 .complete_batch(&first_batch, &first_batch.entries)
4579 .expect("complete first bounded batch");
4580
4581 let second_wake = wake_rx.recv().expect("replacement wake for remaining work");
4582 let second_batch = broker
4583 .take_batch(second_wake)
4584 .expect("second bounded batch");
4585 assert_eq!(second_batch.entries.len(), 1);
4586 }
4587
4588 #[test]
4589 fn readiness_batch_rejects_zero_vm_reactor_work_quantum() {
4590 let mgr = test_manager(1);
4591 let error = SessionReadiness::new(30, &mgr.runtime, 0)
4592 .expect_err("zero work quantum must fail closed");
4593 assert!(error.contains("limits.reactor.workQuantum"));
4594 }
4595
4596 #[test]
4597 fn readiness_rejects_a_stale_capability_generation_without_replacing_state() {
4598 let mgr = test_manager(1);
4599 let (broker, wake_rx) = SessionReadiness::new(13, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4600 .expect("create session readiness");
4601 broker
4602 .publish(9, 4, ReadyFlags::READABLE)
4603 .expect("publish live capability");
4604 let error = broker
4605 .publish(9, 3, ReadyFlags::CLOSE)
4606 .expect_err("stale capability generation must fail");
4607 assert!(error.contains("ERR_AGENTOS_READY_STALE_CAPABILITY"));
4608 assert!(wake_rx.len() <= 1, "wake lane must stay capacity one");
4609 let wake = wake_rx.recv().expect("durable wake");
4610 let batch = broker.take_batch(wake).expect("take readiness batch");
4611 assert_eq!(batch.entries.len(), 1);
4612 assert_eq!(batch.entries[0].capability_generation, 4);
4613 assert_eq!(batch.entries[0].flags, ReadyFlags::READABLE);
4614 }
4615
4616 #[test]
4617 fn readiness_before_guest_registration_remains_pending_until_dispatch_succeeds() {
4618 let mgr = test_manager(1);
4619 let (broker, wake_rx) = SessionReadiness::new(17, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4620 .expect("create session readiness");
4621 broker
4622 .publish(33, 8, ReadyFlags::READABLE)
4623 .expect("publish readiness before guest registration");
4624
4625 let first_wake = wake_rx.recv().expect("initial wake");
4626 let first_batch = broker.take_batch(first_wake).expect("initial batch");
4627 broker
4628 .complete_batch(&first_batch, &[])
4629 .expect("preserve readiness when guest target is absent");
4630 assert_eq!(
4631 broker.broker.pending_handle_count().expect("pending count"),
4632 1
4633 );
4634
4635 let retry_wake = wake_rx.recv().expect("retry wake after registration");
4636 let retry_batch = broker.take_batch(retry_wake).expect("retry batch");
4637 broker
4638 .complete_batch(&retry_batch, &retry_batch.entries)
4639 .expect("acknowledge readiness after target runs");
4640 assert_eq!(
4641 broker.broker.pending_handle_count().expect("pending count"),
4642 0
4643 );
4644 }
4645
4646 #[test]
4647 fn readiness_dispatch_failure_completes_wake_before_session_reuse() {
4648 let mgr = test_manager(1);
4649 let (broker, wake_rx) = SessionReadiness::new(18, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4650 .expect("create reusable session readiness");
4651 broker
4652 .publish(34, 9, ReadyFlags::READABLE)
4653 .expect("publish readiness before failing dispatch");
4654
4655 let first_wake = wake_rx.recv().expect("initial wake");
4656 let first_batch = broker.take_batch(first_wake).expect("initial batch");
4657 let status = complete_ready_batch_dispatch(
4658 &broker,
4659 &first_batch,
4660 &[],
4661 EventLoopStatus::Failed(
4662 1,
4663 ExecutionError {
4664 error_type: String::from("Error"),
4665 message: String::from("injected readiness handler failure"),
4666 stack: String::new(),
4667 code: Some(String::from("ERR_TEST_READY_DISPATCH")),
4668 },
4669 ),
4670 );
4671 assert!(matches!(status, EventLoopStatus::Failed(1, _)));
4672
4673 let retry_wake = wake_rx
4674 .try_recv()
4675 .expect("failed dispatch must complete and rearm the wake for session reuse");
4676 let retry_batch = broker.take_batch(retry_wake).expect("reused-session batch");
4677 assert_eq!(retry_batch.entries, first_batch.entries);
4678 broker
4679 .complete_batch(&retry_batch, &retry_batch.entries)
4680 .expect("reused session can acknowledge the retried readiness");
4681 }
4682
4683 #[test]
4696 fn reset_pending_promises_drops_resolver_globals_before_isolate_teardown() {
4697 use crate::bridge::{register_async_bridge_fns, PendingPromises};
4698 use crate::host_call::BridgeCallContext;
4699 use crate::isolate;
4700 use std::process::Command;
4701
4702 const SUBPROCESS_ENV: &str = "AGENTOS_V8_RESET_PENDING_PROMISES_SUBPROCESS";
4707 if std::env::var_os(SUBPROCESS_ENV).is_none() {
4708 let output = Command::new(std::env::current_exe().expect("current test binary"))
4709 .arg("session::tests::reset_pending_promises_drops_resolver_globals_before_isolate_teardown")
4710 .arg("--exact")
4711 .arg("--nocapture")
4712 .env(SUBPROCESS_ENV, "1")
4713 .output()
4714 .expect("spawn reset-pending-promises subprocess");
4715 assert!(
4716 output.status.success(),
4717 "reset-pending-promises subprocess failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
4718 output.status.code(),
4719 String::from_utf8_lossy(&output.stdout),
4720 String::from_utf8_lossy(&output.stderr)
4721 );
4722 return;
4723 }
4724
4725 isolate::init_v8_platform();
4726
4727 let mut v8_isolate = isolate::create_isolate(None);
4728 let context = isolate::create_context(&mut v8_isolate);
4729 let scope = &mut v8::HandleScope::new(&mut v8_isolate);
4730 let context = v8::Local::new(scope, &context);
4731 let scope = &mut v8::ContextScope::new(scope, context);
4732
4733 let bridge_ctx = BridgeCallContext::new(
4734 Box::new(std::io::sink()),
4735 Box::new(std::io::empty()),
4736 String::from("reset-pending-test"),
4737 );
4738 let mut pending = PendingPromises::new();
4739
4740 const REGISTERED: usize = 8;
4745 let _async_fns = register_async_bridge_fns(
4746 scope,
4747 &bridge_ctx as *const BridgeCallContext,
4748 &pending as *const PendingPromises,
4749 &["_asyncFn"],
4750 );
4751 let source = format!("for (let i = 0; i < {REGISTERED}; i++) {{ _asyncFn(i); }}");
4752 {
4753 let tc = &mut v8::TryCatch::new(scope);
4754 let code = v8::String::new(tc, &source).unwrap();
4755 let script = v8::Script::compile(tc, code, None).unwrap();
4756 assert!(
4757 script.run(tc).is_some(),
4758 "async bridge calls should register resolvers, not throw"
4759 );
4760 assert!(!tc.has_caught(), "async bridge calls should not throw");
4761 }
4762 assert_eq!(
4763 pending.len(),
4764 REGISTERED,
4765 "each _asyncFn call must register a pending resolver Global"
4766 );
4767
4768 reset_pending_promises(&mut pending);
4772
4773 assert_eq!(
4774 pending.len(),
4775 0,
4776 "reset_pending_promises must drop all pending resolver Globals before isolate teardown"
4777 );
4778
4779 }
4782}