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;
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: 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 *active >= maximum {
1069 return Err(format!(
1070 "ERR_AGENTOS_VM_EXECUTOR_LIMIT: active V8 executors reached limit of {maximum}; raise runtime.executor.maxActiveVms"
1071 ));
1072 }
1073 *active += 1;
1074 metrics.observe_executor(ExecutorMetricClass::Vm, *active, 0);
1075 Ok(Self {
1076 control: Arc::clone(control),
1077 metrics,
1078 })
1079 }
1080}
1081
1082impl Drop for SessionSlotPermit {
1083 fn drop(&mut self) {
1084 let (lock, cvar) = &*self.control;
1085 match lock.lock() {
1086 Ok(mut active) if *active > 0 => {
1087 *active -= 1;
1088 self.metrics
1089 .observe_executor(ExecutorMetricClass::Vm, *active, 0);
1090 cvar.notify_all();
1091 }
1092 Ok(_) => eprintln!(
1093 "ERR_AGENTOS_VM_EXECUTOR_ACCOUNTING_UNDERFLOW: executor permit released at zero"
1094 ),
1095 Err(_) => {
1096 eprintln!("ERR_AGENTOS_VM_EXECUTOR_POISONED: executor permit could not be released")
1097 }
1098 }
1099 }
1100}
1101
1102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1103pub(crate) enum ExecutionAbortReason {
1104 Terminated,
1106 #[cfg_attr(test, allow(dead_code))]
1111 WallClockTimedOut,
1112 #[cfg_attr(test, allow(dead_code))]
1114 CpuBudgetExceeded,
1115}
1116
1117struct ExecutionAbortState {
1118 sender: Option<crossbeam_channel::Sender<()>>,
1119 reason: Option<ExecutionAbortReason>,
1120}
1121
1122pub(crate) struct SharedExecutionAbort(Arc<Mutex<Option<ExecutionAbortState>>>);
1123
1124impl Clone for SharedExecutionAbort {
1125 fn clone(&self) -> Self {
1126 Self(Arc::clone(&self.0))
1127 }
1128}
1129
1130pub(crate) fn new_execution_abort() -> SharedExecutionAbort {
1131 SharedExecutionAbort(Arc::new(Mutex::new(None)))
1132}
1133
1134#[cfg_attr(test, allow(dead_code))]
1135pub(crate) struct ActiveExecutionAbort {
1136 shared: SharedExecutionAbort,
1137}
1138
1139#[cfg_attr(test, allow(dead_code))]
1140impl ActiveExecutionAbort {
1141 pub(crate) fn arm(shared: &SharedExecutionAbort) -> (Self, crossbeam_channel::Receiver<()>) {
1142 let (tx, rx) = crossbeam_channel::bounded::<()>(0);
1143 let mut guard = shared.0.lock().unwrap();
1144 if let Some(reason) = guard.as_ref().and_then(|state| state.reason) {
1145 drop(tx);
1150 *guard = Some(ExecutionAbortState {
1151 sender: None,
1152 reason: Some(reason),
1153 });
1154 } else {
1155 *guard = Some(ExecutionAbortState {
1156 sender: Some(tx),
1157 reason: None,
1158 });
1159 }
1160 (
1161 Self {
1162 shared: shared.clone(),
1163 },
1164 rx,
1165 )
1166 }
1167}
1168
1169impl Drop for ActiveExecutionAbort {
1170 fn drop(&mut self) {
1171 *self.shared.0.lock().unwrap() = None;
1172 }
1173}
1174
1175struct ExecutionActivityGuard(Arc<AtomicBool>);
1176
1177impl Drop for ExecutionActivityGuard {
1178 fn drop(&mut self) {
1179 self.0.store(false, Ordering::Release);
1180 }
1181}
1182
1183pub(crate) fn signal_execution_abort(shared: &SharedExecutionAbort, reason: ExecutionAbortReason) {
1184 let mut guard = shared.0.lock().unwrap();
1185 if let Some(state) = guard.as_mut() {
1186 state.reason.get_or_insert(reason);
1187 state.sender.take();
1188 }
1189}
1190
1191fn signal_execution_abort_durable(shared: &SharedExecutionAbort, reason: ExecutionAbortReason) {
1192 let mut guard = shared.0.lock().unwrap();
1193 if let Some(state) = guard.as_mut() {
1194 state.reason.get_or_insert(reason);
1195 state.sender.take();
1196 } else {
1197 *guard = Some(ExecutionAbortState {
1201 sender: None,
1202 reason: Some(reason),
1203 });
1204 }
1205}
1206
1207#[cfg(not(test))]
1208fn execution_abort_reason(shared: &SharedExecutionAbort) -> Option<ExecutionAbortReason> {
1209 shared
1210 .0
1211 .lock()
1212 .unwrap()
1213 .as_ref()
1214 .and_then(|state| state.reason)
1215}
1216
1217pub struct SessionManager {
1220 sessions: HashMap<String, SessionEntry>,
1221 quarantined: Vec<QuarantinedSession>,
1225 max_concurrency: usize,
1226 slot_control: SlotControl,
1227 event_tx: RuntimeEventSender,
1229 call_id_router: CallIdRouter,
1231 shared_call_id: SharedCallIdCounter,
1234 snapshot_cache: Arc<SnapshotCache>,
1236 warm_pool: Arc<WarmWorkerPool>,
1238 runtime: RuntimeContext,
1241 executor_teardown_timeout: Duration,
1242}
1243
1244struct QuarantinedSession {
1245 session_id: String,
1246 output_generation: Option<u64>,
1247 join_handle: thread::JoinHandle<()>,
1248 quarantined_at: Instant,
1249 deadline_reported: bool,
1250}
1251
1252impl SessionManager {
1253 pub fn new(
1254 max_concurrency: usize,
1255 event_tx: impl Into<RuntimeEventSender>,
1256 call_id_router: CallIdRouter,
1257 snapshot_cache: Arc<SnapshotCache>,
1258 runtime: RuntimeContext,
1259 ) -> Self {
1260 SessionManager {
1261 sessions: HashMap::new(),
1262 quarantined: Vec::new(),
1263 max_concurrency,
1264 slot_control: Arc::new((Mutex::new(0), Condvar::new())),
1265 event_tx: event_tx.into(),
1266 call_id_router,
1267 shared_call_id: Arc::new(AtomicU64::new(1)),
1268 snapshot_cache,
1269 warm_pool: Arc::new(WarmWorkerPool::default()),
1270 executor_teardown_timeout: runtime.vm_executor_teardown_timeout(),
1271 runtime,
1272 }
1273 }
1274
1275 #[cfg(test)]
1276 pub(crate) fn max_concurrency(&self) -> usize {
1277 self.max_concurrency
1278 }
1279
1280 #[allow(dead_code)]
1282 pub fn snapshot_cache(&self) -> &Arc<SnapshotCache> {
1283 &self.snapshot_cache
1284 }
1285
1286 pub fn pre_warm_workers(
1287 &self,
1288 bridge_code: String,
1289 userland_code: String,
1290 heap_limit_mb: Option<u32>,
1291 count: usize,
1292 ) {
1293 self.warm_pool.ensure_count(
1294 self.runtime.clone(),
1295 Arc::clone(&self.snapshot_cache),
1296 Arc::clone(&self.slot_control),
1297 bridge_code,
1298 userland_code,
1299 heap_limit_mb,
1300 count,
1301 );
1302 }
1303
1304 pub fn create_session(
1308 &mut self,
1309 session_id: String,
1310 heap_limit_mb: Option<u32>,
1311 cpu_time_limit_ms: Option<u32>,
1312 wall_clock_limit_ms: Option<u32>,
1313 ) -> Result<(), String> {
1314 self.create_session_with_output_generation(
1315 session_id,
1316 heap_limit_mb,
1317 cpu_time_limit_ms,
1318 wall_clock_limit_ms,
1319 None,
1320 None,
1321 )
1322 }
1323
1324 pub fn create_session_with_output_generation(
1325 &mut self,
1326 session_id: String,
1327 heap_limit_mb: Option<u32>,
1328 cpu_time_limit_ms: Option<u32>,
1329 wall_clock_limit_ms: Option<u32>,
1330 output_generation: Option<u64>,
1331 warm_hint: Option<WarmSessionHint>,
1332 ) -> Result<(), String> {
1333 self.create_session_with_output_generation_and_sender(
1334 session_id,
1335 heap_limit_mb,
1336 cpu_time_limit_ms,
1337 wall_clock_limit_ms,
1338 output_generation,
1339 warm_hint,
1340 None,
1341 )
1342 }
1343
1344 #[allow(clippy::too_many_arguments)]
1345 pub fn create_session_with_output_generation_and_sender(
1346 &mut self,
1347 session_id: String,
1348 heap_limit_mb: Option<u32>,
1349 cpu_time_limit_ms: Option<u32>,
1350 wall_clock_limit_ms: Option<u32>,
1351 output_generation: Option<u64>,
1352 warm_hint: Option<WarmSessionHint>,
1353 event_tx: Option<RuntimeEventSender>,
1354 ) -> Result<(), String> {
1355 let ready_batch_handle_limit = configured_resource_capacity(
1359 &self.runtime,
1360 ResourceClass::ReadyHandles,
1361 "limits.reactor.maxReadyHandles",
1362 "runtime.resources.maxReadyHandles",
1363 )?;
1364 self.create_session_with_output_generation_sender_and_runtime(
1365 session_id,
1366 heap_limit_mb,
1367 cpu_time_limit_ms,
1368 wall_clock_limit_ms,
1369 output_generation,
1370 warm_hint,
1371 event_tx,
1372 self.runtime.clone(),
1373 ready_batch_handle_limit,
1374 crate::host_call::DEFAULT_BRIDGE_CALL_TIMEOUT,
1375 )
1376 }
1377
1378 #[allow(clippy::too_many_arguments)]
1382 pub fn create_session_with_output_generation_sender_and_runtime(
1383 &mut self,
1384 session_id: String,
1385 heap_limit_mb: Option<u32>,
1386 cpu_time_limit_ms: Option<u32>,
1387 wall_clock_limit_ms: Option<u32>,
1388 output_generation: Option<u64>,
1389 warm_hint: Option<WarmSessionHint>,
1390 event_tx: Option<RuntimeEventSender>,
1391 session_runtime: RuntimeContext,
1392 ready_batch_handle_limit: usize,
1393 bridge_call_timeout: Duration,
1394 ) -> Result<(), String> {
1395 self.reap_finished_quarantines();
1396 if self.sessions.contains_key(&session_id) {
1397 return Err(format!("session {} already exists", session_id));
1398 }
1399
1400 let slot_permit = SessionSlotPermit::try_acquire(
1401 &self.slot_control,
1402 self.max_concurrency,
1403 self.runtime.metrics().clone(),
1404 )?;
1405
1406 let cpu_time_limit_ms = normalize_cpu_time_limit_ms(cpu_time_limit_ms);
1407 let wall_clock_limit_ms = normalize_wall_clock_limit_ms(wall_clock_limit_ms);
1408 let command_capacity = configured_resource_capacity(
1409 &session_runtime,
1410 ResourceClass::HandleCommands,
1411 "limits.reactor.maxHandleCommands",
1412 "runtime.resources.maxHandleCommands",
1413 )?;
1414 let (tx, rx) = crossbeam_channel::bounded(command_capacity);
1415 let (shutdown_tx, shutdown_rx) = crossbeam_channel::bounded(1);
1416 let ready_generation = output_generation.unwrap_or(1);
1417 let (ready_broker, ready_rx) =
1418 SessionReadiness::new(ready_generation, &session_runtime, ready_batch_handle_limit)?;
1419 let isolate_handle = Arc::new(Mutex::new(None));
1420 let execution_abort = new_execution_abort();
1421 let execution_active = Arc::new(AtomicBool::new(false));
1422 let pause_control = Arc::new(SessionPauseControl::default());
1423 #[cfg(test)]
1424 let session_resources = Arc::clone(session_runtime.resources());
1425 let assignment = SessionAssignment {
1426 heap_limit_mb,
1427 cpu_time_limit_ms,
1428 wall_clock_limit_ms,
1429 rx,
1430 shutdown_rx,
1431 ready_rx,
1432 ready_broker: Arc::clone(&ready_broker),
1433 slot_permit,
1434 event_tx: event_tx.unwrap_or_else(|| self.event_tx.clone()),
1435 call_id_router: Arc::clone(&self.call_id_router),
1436 shared_call_id: Arc::clone(&self.shared_call_id),
1437 snapshot_cache: Arc::clone(&self.snapshot_cache),
1438 isolate_handle: Arc::clone(&isolate_handle),
1439 execution_abort: execution_abort.clone(),
1440 execution_active: Arc::clone(&execution_active),
1441 pause_control: Arc::clone(&pause_control),
1442 session_id: session_id.clone(),
1443 output_generation,
1444 runtime: session_runtime,
1445 bridge_call_timeout,
1446 };
1447
1448 let join_handle = match self.claim_warm_worker(warm_hint.as_ref(), assignment) {
1449 Ok((join_handle, true)) => {
1450 if let Some(hint) = warm_hint {
1451 self.warm_pool.ensure_count(
1452 self.runtime.clone(),
1453 Arc::clone(&self.snapshot_cache),
1454 Arc::clone(&self.slot_control),
1455 hint.bridge_code,
1456 hint.userland_code,
1457 hint.heap_limit_mb,
1458 warm_worker_capacity_per_key(),
1459 );
1460 }
1461 join_handle
1462 }
1463 Ok((join_handle, false)) => join_handle,
1464 Err(assignment) => {
1465 if let Some(hint) = warm_hint {
1466 self.warm_pool.ensure_count(
1467 self.runtime.clone(),
1468 Arc::clone(&self.snapshot_cache),
1469 Arc::clone(&self.slot_control),
1470 hint.bridge_code,
1471 hint.userland_code,
1472 hint.heap_limit_mb,
1473 warm_worker_capacity_per_key(),
1474 );
1475 }
1476 spawn_session_thread(assignment)
1477 .map_err(|e| format!("failed to spawn session thread: {}", e))?
1478 }
1479 };
1480
1481 self.sessions.insert(
1482 session_id,
1483 SessionEntry {
1484 output_generation,
1485 tx,
1486 command_capacity,
1487 shutdown_tx,
1488 join_handle: Some(join_handle),
1489 isolate_handle,
1490 execution_abort,
1491 execution_active,
1492 pause_control,
1493 ready_broker,
1494 #[cfg(test)]
1495 session_resources,
1496 },
1497 );
1498
1499 Ok(())
1500 }
1501
1502 #[allow(clippy::result_large_err)]
1505 fn claim_warm_worker(
1506 &self,
1507 warm_hint: Option<&WarmSessionHint>,
1508 assignment: SessionAssignment,
1509 ) -> Result<(thread::JoinHandle<()>, bool), SessionAssignment> {
1510 let Some(hint) = warm_hint else {
1511 return Err(assignment);
1512 };
1513 if warm_worker_capacity_per_key() == 0 {
1514 record_warm_worker_miss();
1515 return Err(assignment);
1516 }
1517
1518 let key = warm_pool_key(&hint.bridge_code, &hint.userland_code, hint.heap_limit_mb);
1519 let Some(worker) = self.warm_pool.claim(&key) else {
1520 record_warm_worker_miss();
1521 eprintln!(
1522 "agentos-v8-runtime: warm worker pool-empty key={} heap={}",
1523 warm_key_prefix(&key),
1524 key.heap_limit_mb
1525 );
1526 return Err(assignment);
1527 };
1528
1529 match worker.assignment_tx.send(assignment) {
1530 Ok(()) => {
1531 record_warm_worker_hit();
1532 eprintln!(
1533 "agentos-v8-runtime: warm worker claimed key={} heap={}",
1534 warm_key_prefix(&key),
1535 key.heap_limit_mb
1536 );
1537 Ok((worker.join_handle, true))
1538 }
1539 Err(error) => {
1540 record_warm_worker_miss();
1541 let _ = worker.join_handle.join();
1542 Err(error.0)
1543 }
1544 }
1545 }
1546
1547 pub fn destroy_session_if_output_generation(
1548 &mut self,
1549 session_id: &str,
1550 output_generation: u64,
1551 ) -> Result<bool, String> {
1552 match self.begin_destroy_session_if_output_generation(session_id, output_generation)? {
1553 Some(shutdown) => {
1554 shutdown.finish();
1555 Ok(true)
1556 }
1557 None => Ok(false),
1558 }
1559 }
1560
1561 pub fn begin_destroy_session_if_output_generation(
1562 &mut self,
1563 session_id: &str,
1564 output_generation: u64,
1565 ) -> Result<Option<SessionShutdown>, String> {
1566 if self
1567 .sessions
1568 .get(session_id)
1569 .is_none_or(|entry| entry.output_generation != Some(output_generation))
1570 {
1571 return Ok(None);
1572 }
1573
1574 self.begin_destroy_session(session_id).map(Some)
1575 }
1576
1577 pub fn detach_session_if_output_generation(
1578 &mut self,
1579 session_id: &str,
1580 output_generation: u64,
1581 ) -> Result<bool, String> {
1582 if self
1583 .sessions
1584 .get(session_id)
1585 .is_none_or(|entry| entry.output_generation != Some(output_generation))
1586 {
1587 return Ok(false);
1588 }
1589
1590 self.detach_session(session_id)?;
1591 Ok(true)
1592 }
1593
1594 pub(crate) fn detach_session(&mut self, session_id: &str) -> Result<(), String> {
1595 let entry = self
1596 .sessions
1597 .get(session_id)
1598 .ok_or_else(|| format!("session {} does not exist", session_id))?;
1599 entry.pause_control.shutdown();
1600
1601 #[cfg(not(test))]
1602 if let Some(handle) = entry
1603 .isolate_handle
1604 .lock()
1605 .ok()
1606 .and_then(|guard| guard.as_ref().cloned())
1607 {
1608 handle.terminate_execution();
1609 }
1610 signal_execution_abort_durable(&entry.execution_abort, ExecutionAbortReason::Terminated);
1611 self.clear_call_routes_for_session(session_id, entry.output_generation);
1612 let mut entry = self.sessions.remove(session_id).unwrap();
1613 signal_session_shutdown(&entry.shutdown_tx, session_id);
1614 drop(entry.tx);
1615 if let Some(join_handle) = entry.join_handle.take() {
1616 eprintln!(
1617 "WARN_AGENTOS_VM_EXECUTOR_QUARANTINED: session={} generation={:?}",
1618 session_id, entry.output_generation
1619 );
1620 self.quarantined.push(QuarantinedSession {
1621 session_id: session_id.to_owned(),
1622 output_generation: entry.output_generation,
1623 join_handle,
1624 quarantined_at: Instant::now(),
1625 deadline_reported: false,
1626 });
1627 }
1628 Ok(())
1629 }
1630
1631 fn reap_finished_quarantines(&mut self) {
1632 let mut retained = Vec::with_capacity(self.quarantined.len());
1633 for mut quarantined in self.quarantined.drain(..) {
1634 if !quarantined.join_handle.is_finished() {
1635 if !quarantined.deadline_reported
1636 && quarantined.quarantined_at.elapsed() >= self.executor_teardown_timeout
1637 {
1638 quarantined.deadline_reported = true;
1639 eprintln!(
1640 "ERR_AGENTOS_VM_EXECUTOR_TEARDOWN_TIMEOUT: session={} generation={:?} deadline_ms={}; executor remains quarantined and retains its permit; raise runtime.executor.teardownTimeoutMs",
1641 quarantined.session_id,
1642 quarantined.output_generation,
1643 self.executor_teardown_timeout.as_millis()
1644 );
1645 }
1646 retained.push(quarantined);
1647 continue;
1648 }
1649 if quarantined.join_handle.join().is_err() {
1650 eprintln!(
1651 "ERR_AGENTOS_VM_EXECUTOR_PANIC: quarantined session={} generation={:?}",
1652 quarantined.session_id, quarantined.output_generation
1653 );
1654 } else {
1655 eprintln!(
1656 "INFO_AGENTOS_VM_EXECUTOR_QUARANTINE_RELEASED: session={} generation={:?}",
1657 quarantined.session_id, quarantined.output_generation
1658 );
1659 }
1660 self.call_id_router
1661 .cancel_session(&quarantined.session_id, quarantined.output_generation);
1662 }
1663 self.quarantined = retained;
1664 }
1665
1666 pub fn destroy_session(&mut self, session_id: &str) -> Result<(), String> {
1670 self.begin_destroy_session(session_id)?.finish();
1671 Ok(())
1672 }
1673
1674 pub fn begin_destroy_session(&mut self, session_id: &str) -> Result<SessionShutdown, String> {
1679 if !self.sessions.contains_key(session_id) {
1680 return Err(format!("session {} does not exist", session_id));
1681 }
1682
1683 let output_generation = self
1684 .sessions
1685 .get(session_id)
1686 .and_then(|entry| entry.output_generation);
1687 self.clear_call_routes_for_session(session_id, output_generation);
1688 let mut entry = self
1689 .sessions
1690 .remove(session_id)
1691 .expect("checked session exists");
1692 entry.pause_control.shutdown();
1693
1694 #[cfg(not(test))]
1695 if let Some(handle) = entry
1696 .isolate_handle
1697 .lock()
1698 .ok()
1699 .and_then(|guard| guard.as_ref().cloned())
1700 {
1701 handle.terminate_execution();
1702 }
1703 signal_execution_abort_durable(&entry.execution_abort, ExecutionAbortReason::Terminated);
1704 signal_session_shutdown(&entry.shutdown_tx, session_id);
1707 let join_handle = entry.join_handle.take();
1708 drop(entry);
1709 Ok(SessionShutdown {
1710 session_id: session_id.to_owned(),
1711 output_generation,
1712 join_handle,
1713 call_id_router: Arc::clone(&self.call_id_router),
1714 })
1715 }
1716
1717 pub(crate) fn take_session_shutdown_handles(&mut self) -> Vec<thread::JoinHandle<()>> {
1718 self.call_id_router.clear();
1719
1720 let mut handles: Vec<_> = self
1721 .sessions
1722 .drain()
1723 .filter_map(|(session_id, mut entry)| {
1724 #[cfg(not(test))]
1725 if let Some(handle) = entry
1726 .isolate_handle
1727 .lock()
1728 .ok()
1729 .and_then(|guard| guard.as_ref().cloned())
1730 {
1731 handle.terminate_execution();
1732 }
1733 signal_execution_abort_durable(
1734 &entry.execution_abort,
1735 ExecutionAbortReason::Terminated,
1736 );
1737 signal_session_shutdown(&entry.shutdown_tx, &session_id);
1738 drop(entry.tx);
1739 entry.join_handle.take()
1740 })
1741 .collect();
1742 handles.extend(self.warm_pool.shutdown_handles());
1743 handles.extend(
1744 self.quarantined
1745 .drain(..)
1746 .map(|quarantined| quarantined.join_handle),
1747 );
1748 handles
1749 }
1750
1751 #[cfg(test)]
1752 pub(crate) fn clear_call_route(&self, call_id: u64) {
1753 self.call_id_router.cancel(call_id);
1754 }
1755
1756 fn clear_call_routes_for_session(&self, session_id: &str, output_generation: Option<u64>) {
1757 self.call_id_router
1758 .cancel_session(session_id, output_generation);
1759 }
1760
1761 pub fn session_command_sender(
1766 &self,
1767 session_id: &str,
1768 msg: &SessionMessage,
1769 ) -> Result<(Sender<SessionCommand>, usize, Arc<AtomicBool>), String> {
1770 let entry = self
1771 .sessions
1772 .get(session_id)
1773 .ok_or_else(|| format!("session {} does not exist", session_id))?;
1774
1775 #[cfg(not(test))]
1776 if matches!(msg, SessionMessage::TerminateExecution) {
1777 if let Some(handle) = entry
1778 .isolate_handle
1779 .lock()
1780 .ok()
1781 .and_then(|guard| guard.as_ref().cloned())
1782 {
1783 handle.terminate_execution();
1784 }
1785 }
1786 if matches!(msg, SessionMessage::TerminateExecution) {
1787 if entry.execution_active.load(Ordering::Acquire) {
1788 signal_execution_abort_durable(
1789 &entry.execution_abort,
1790 ExecutionAbortReason::Terminated,
1791 );
1792 } else {
1793 signal_execution_abort(&entry.execution_abort, ExecutionAbortReason::Terminated);
1794 }
1795 }
1796
1797 Ok((
1798 entry.tx.clone(),
1799 entry.command_capacity,
1800 Arc::clone(&entry.execution_active),
1801 ))
1802 }
1803
1804 pub fn try_send_to_session(&self, session_id: &str, msg: SessionMessage) -> Result<(), String> {
1808 let terminate_requested = matches!(&msg, SessionMessage::TerminateExecution);
1809 let execute_requested = matches!(&msg, SessionMessage::Execute { .. });
1810 let incoming_kind = match &msg {
1811 SessionMessage::InjectGlobals { .. } => String::from("inject_globals"),
1812 SessionMessage::Execute { .. } => String::from("execute"),
1813 SessionMessage::BridgeResponse(_) => String::from("bridge_response"),
1814 SessionMessage::StreamEvent(event) => format!("stream_event:{}", event.event_type),
1815 SessionMessage::TerminateExecution => String::from("terminate_execution"),
1816 };
1817 let (sender, command_capacity, execution_active) =
1818 self.session_command_sender(session_id, &msg)?;
1819 if execute_requested {
1820 execution_active.store(true, Ordering::Release);
1821 }
1822 let command = SessionCommand::Message(msg);
1823
1824 match sender.try_send(command) {
1825 Ok(()) => Ok(()),
1826 Err(crossbeam_channel::TrySendError::Full(_)) if terminate_requested => {
1827 Ok(())
1830 }
1831 Err(crossbeam_channel::TrySendError::Full(_)) => {
1832 if execute_requested {
1833 execution_active.store(false, Ordering::Release);
1834 }
1835 Err(format!(
1836 "ERR_AGENTOS_SESSION_COMMAND_LIMIT: session {session_id} command queue exceeded limit of {command_capacity} while admitting {incoming_kind} (queued={}); raise limits.reactor.maxHandleCommands",
1837 sender.len()
1838 ))
1839 }
1840 Err(crossbeam_channel::TrySendError::Disconnected(_)) => {
1841 if execute_requested {
1842 execution_active.store(false, Ordering::Release);
1843 }
1844 Err(format!(
1845 "session thread disconnected for session {session_id}"
1846 ))
1847 }
1848 }
1849 }
1850
1851 pub fn publish_readiness(
1852 &self,
1853 session_id: &str,
1854 capability_id: u64,
1855 capability_generation: u64,
1856 flags: ReadyFlags,
1857 ) -> Result<(), String> {
1858 let entry = self
1859 .sessions
1860 .get(session_id)
1861 .ok_or_else(|| format!("session {session_id} does not exist"))?;
1862 entry
1863 .ready_broker
1864 .publish(capability_id, capability_generation, flags)
1865 }
1866
1867 pub fn publish_signal(&self, session_id: &str, signal: i32) -> Result<(), String> {
1868 let entry = self
1869 .sessions
1870 .get(session_id)
1871 .ok_or_else(|| format!("session {session_id} does not exist"))?;
1872 entry.ready_broker.publish_signal(signal)
1873 }
1874
1875 pub fn remove_readiness(
1876 &self,
1877 session_id: &str,
1878 capability_id: u64,
1879 capability_generation: u64,
1880 ) -> Result<(), String> {
1881 let entry = self
1882 .sessions
1883 .get(session_id)
1884 .ok_or_else(|| format!("session {session_id} does not exist"))?;
1885 entry
1886 .ready_broker
1887 .remove(capability_id, capability_generation)
1888 }
1889
1890 pub fn set_application_read_interest(
1891 &self,
1892 session_id: &str,
1893 capability_id: u64,
1894 capability_generation: u64,
1895 enabled: bool,
1896 ) -> Result<(), String> {
1897 let entry = self
1898 .sessions
1899 .get(session_id)
1900 .ok_or_else(|| format!("session {session_id} does not exist"))?;
1901 entry.ready_broker.set_application_read_interest(
1902 capability_id,
1903 capability_generation,
1904 enabled,
1905 )
1906 }
1907
1908 pub fn publish_timer(&self, session_id: &str, timer_id: u64) -> Result<(), String> {
1909 let entry = self
1910 .sessions
1911 .get(session_id)
1912 .ok_or_else(|| format!("session {session_id} does not exist"))?;
1913 entry.ready_broker.publish_timer(timer_id)
1914 }
1915
1916 pub fn session_sender(
1919 &self,
1920 session_id: &str,
1921 ) -> Result<(Sender<SessionCommand>, usize), String> {
1922 self.sessions
1923 .get(session_id)
1924 .map(|entry| (entry.tx.clone(), entry.command_capacity))
1925 .ok_or_else(|| format!("session {} does not exist", session_id))
1926 }
1927
1928 pub fn pause_session(&self, session_id: &str) -> Result<(), String> {
1932 let entry = self
1933 .sessions
1934 .get(session_id)
1935 .ok_or_else(|| format!("session {} does not exist", session_id))?;
1936 entry.pause_control.pause();
1937 #[cfg(not(test))]
1938 if let Some(handle) = entry
1939 .isolate_handle
1940 .lock()
1941 .ok()
1942 .and_then(|guard| guard.as_ref().cloned())
1943 {
1944 let raw = Arc::into_raw(Arc::clone(&entry.pause_control)) as *mut std::ffi::c_void;
1945 if !handle.request_interrupt(pause_isolate_interrupt, raw) {
1946 unsafe { drop(Arc::from_raw(raw.cast::<SessionPauseControl>())) };
1949 }
1950 }
1951 Ok(())
1952 }
1953
1954 pub fn resume_session(&self, session_id: &str) -> Result<(), String> {
1955 let entry = self
1956 .sessions
1957 .get(session_id)
1958 .ok_or_else(|| format!("session {} does not exist", session_id))?;
1959 entry.pause_control.resume();
1960 Ok(())
1961 }
1962
1963 pub fn send_to_session(&self, session_id: &str, msg: SessionMessage) -> Result<(), String> {
1965 self.try_send_to_session(session_id, msg)
1966 }
1967
1968 pub fn destroy_sessions<I>(&mut self, session_ids: I)
1972 where
1973 I: IntoIterator<Item = String>,
1974 {
1975 for shutdown in self.begin_destroy_sessions(session_ids) {
1976 shutdown.finish();
1977 }
1978 }
1979
1980 pub fn begin_destroy_sessions<I>(&mut self, session_ids: I) -> Vec<SessionShutdown>
1983 where
1984 I: IntoIterator<Item = String>,
1985 {
1986 session_ids
1987 .into_iter()
1988 .filter_map(|sid| self.begin_destroy_session(&sid).ok())
1989 .collect()
1990 }
1991
1992 #[allow(dead_code)]
1994 pub fn session_count(&self) -> usize {
1995 self.sessions.len()
1996 }
1997
1998 #[allow(dead_code)]
1999 pub fn quarantined_session_count(&mut self) -> usize {
2000 self.reap_finished_quarantines();
2001 self.quarantined.len()
2002 }
2003
2004 #[allow(dead_code)]
2006 pub fn all_sessions(&self) -> Vec<String> {
2007 self.sessions.keys().cloned().collect()
2008 }
2009
2010 #[allow(dead_code)]
2012 pub fn active_slot_count(&self) -> usize {
2013 let (lock, _) = &*self.slot_control;
2014 *lock.lock().unwrap()
2015 }
2016
2017 pub fn session_output_generation(&self, session_id: &str) -> Option<u64> {
2018 self.sessions
2019 .get(session_id)
2020 .and_then(|entry| entry.output_generation)
2021 }
2022
2023 #[cfg(test)]
2024 pub fn session_resources(
2025 &self,
2026 session_id: &str,
2027 ) -> Option<Arc<agentos_runtime::accounting::ResourceLedger>> {
2028 self.sessions
2029 .get(session_id)
2030 .map(|entry| Arc::clone(&entry.session_resources))
2031 }
2032
2033 pub fn call_id_router(&self) -> &CallIdRouter {
2035 &self.call_id_router
2036 }
2037}
2038
2039#[cfg(not(test))]
2041fn send_event_with_generation(
2042 event_tx: &RuntimeEventSender,
2043 output_generation: Option<u64>,
2044 event: RuntimeEvent,
2045) {
2046 if let Err(error) = event_tx.send(RuntimeEventEnvelope {
2047 output_generation,
2048 event,
2049 }) {
2050 eprintln!("failed to send runtime event: {error}");
2051 }
2052}
2053
2054fn send_late_message_warning(
2055 event_tx: &RuntimeEventSender,
2056 session_id: &str,
2057 output_generation: Option<u64>,
2058 error_code: &str,
2059 detail: String,
2060) {
2061 let warning = RuntimeEvent::Log {
2062 session_id: session_id.to_string(),
2063 channel: 1,
2064 message: format!("[{error_code}] {detail}"),
2065 };
2066 if let Err(error) = event_tx.send(RuntimeEventEnvelope {
2067 output_generation,
2068 event: warning,
2069 }) {
2070 eprintln!("failed to send late-session warning: {error}");
2071 }
2072}
2073
2074fn handle_late_session_message(
2075 event_tx: &RuntimeEventSender,
2076 session_id: &str,
2077 output_generation: Option<u64>,
2078 message: SessionMessage,
2079) {
2080 match message {
2081 SessionMessage::BridgeResponse(BridgeResponse {
2082 call_id,
2083 status,
2084 payload,
2085 reservation: _,
2086 }) => send_late_message_warning(
2087 event_tx,
2088 session_id,
2089 output_generation,
2090 LATE_BRIDGE_RESPONSE_ERROR_CODE,
2091 format!(
2092 "dropping BridgeResponse after execution completed (call_id={call_id}, status={status}, payload_len={})",
2093 payload.len()
2094 ),
2095 ),
2096 SessionMessage::StreamEvent(StreamEvent {
2097 event_type,
2098 payload,
2099 }) => {
2100 if event_type == "timer" || event_type == "net_socket" || event_type == "stdin_end" {
2106 return;
2107 }
2108 send_late_message_warning(
2109 event_tx,
2110 session_id,
2111 output_generation,
2112 LATE_STREAM_EVENT_ERROR_CODE,
2113 format!(
2114 "dropping StreamEvent after execution completed (event_type={event_type}, payload_len={})",
2115 payload.len()
2116 ),
2117 )
2118 }
2119 SessionMessage::TerminateExecution => send_late_message_warning(
2120 event_tx,
2121 session_id,
2122 output_generation,
2123 LATE_TERMINATE_EXECUTION_ERROR_CODE,
2124 String::from("dropping TerminateExecution after execution completed"),
2125 ),
2126 SessionMessage::InjectGlobals { .. } | SessionMessage::Execute { .. } => {}
2127 }
2128}
2129
2130#[cfg(not(test))]
2131fn install_wasm_module_bytes_global<'s>(scope: &mut v8::HandleScope<'s>, bytes: &[u8]) -> bool {
2132 let global = scope.get_current_context().global(scope);
2133 let Some(name) = v8::String::new(scope, "__agentOSWasmModuleBytes") else {
2134 return false;
2135 };
2136 let len = bytes.len();
2137 let backing_store = v8::ArrayBuffer::new_backing_store_from_bytes(bytes.to_vec());
2138 let array_buffer = v8::ArrayBuffer::with_backing_store(scope, &backing_store.make_shared());
2139 let Some(bytes_value) = v8::Uint8Array::new(scope, array_buffer, 0, len) else {
2140 return false;
2141 };
2142 global.set(scope, name.into(), bytes_value.into()).is_some()
2143}
2144
2145fn spawn_session_thread(assignment: SessionAssignment) -> std::io::Result<thread::JoinHandle<()>> {
2149 let name_prefix = if assignment.session_id.len() > 8 {
2150 assignment.session_id[..8].to_string()
2151 } else {
2152 assignment.session_id.clone()
2153 };
2154 thread::Builder::new()
2156 .name(format!("session-{}", name_prefix))
2157 .spawn(move || session_thread(assignment, None))
2158}
2159
2160fn recv_session_command(
2161 rx: &Receiver<SessionCommand>,
2162 shutdown_rx: &Receiver<()>,
2163 ready_rx: &Receiver<ReadyWake>,
2164 ready_broker: &SessionReadiness,
2165) -> Option<SessionCommand> {
2166 loop {
2167 match shutdown_rx.try_recv() {
2174 Ok(()) | Err(crossbeam_channel::TryRecvError::Disconnected) => {
2175 return Some(SessionCommand::Shutdown);
2176 }
2177 Err(crossbeam_channel::TryRecvError::Empty) => {}
2178 }
2179 match rx.try_recv() {
2180 Ok(command) => return Some(command),
2181 Err(crossbeam_channel::TryRecvError::Disconnected) => return None,
2182 Err(crossbeam_channel::TryRecvError::Empty) => {}
2183 }
2184 match ready_rx.try_recv() {
2185 Ok(wake) => match ready_batch_command(ready_broker, wake) {
2186 Ok(command) => return Some(command),
2187 Err(error) => {
2188 eprintln!("{error}");
2189 continue;
2190 }
2191 },
2192 Err(crossbeam_channel::TryRecvError::Disconnected) => return None,
2193 Err(crossbeam_channel::TryRecvError::Empty) => {}
2194 }
2195
2196 crossbeam_channel::select! {
2197 recv(shutdown_rx) -> _ => return Some(SessionCommand::Shutdown),
2198 recv(ready_rx) -> wake => {
2199 let wake = wake.ok()?;
2200 match ready_batch_command(ready_broker, wake) {
2201 Ok(command) => return Some(command),
2202 Err(error) => {
2203 eprintln!("{error}");
2204 continue;
2205 }
2206 }
2207 },
2208 recv(rx) -> command => return command.ok(),
2209 }
2210 }
2211}
2212
2213fn ready_batch_command(
2217 ready_broker: &SessionReadiness,
2218 wake: ReadyWake,
2219) -> Result<SessionCommand, String> {
2220 ready_broker
2221 .take_batch(wake)
2222 .map(SessionCommand::ReadyBatch)
2223}
2224
2225#[allow(clippy::too_many_arguments)]
2226fn session_thread(
2227 assignment: SessionAssignment,
2228 #[cfg_attr(test, allow(unused_variables))] precreated_isolate: Option<PrecreatedIsolate>,
2229) {
2230 let SessionAssignment {
2231 heap_limit_mb,
2232 cpu_time_limit_ms,
2233 wall_clock_limit_ms,
2234 rx,
2235 shutdown_rx,
2236 ready_rx,
2237 ready_broker,
2238 slot_permit: _slot_permit,
2239 event_tx,
2240 call_id_router,
2241 shared_call_id,
2242 snapshot_cache,
2243 isolate_handle,
2244 execution_abort,
2245 execution_active,
2246 pause_control,
2247 session_id,
2248 output_generation,
2249 runtime,
2250 bridge_call_timeout,
2251 } = assignment;
2252 #[cfg(not(test))]
2253 let execution_task_owner =
2254 output_generation.map(|generation| agentos_runtime::TaskOwner::Vm { generation });
2255 #[cfg(test)]
2256 let _ = (
2257 heap_limit_mb,
2258 cpu_time_limit_ms,
2259 wall_clock_limit_ms,
2260 call_id_router,
2261 shared_call_id,
2262 snapshot_cache,
2263 isolate_handle,
2264 execution_abort,
2265 &pause_control,
2266 &ready_broker,
2267 &runtime,
2268 bridge_call_timeout,
2269 );
2270
2271 #[cfg(all(not(test), unix))]
2277 let exec_thread_cpu_clock = crate::timeout::current_thread_cpu_clock();
2278 #[cfg(all(not(test), not(unix)))]
2279 let exec_thread_cpu_clock: Option<crate::timeout::ThreadCpuClock> = None;
2280
2281 #[cfg(not(test))]
2285 let (
2286 mut v8_isolate,
2287 mut _v8_context,
2288 mut from_snapshot,
2289 mut isolate_bridge_code,
2290 mut isolate_userland_code,
2291 ) = match precreated_isolate {
2292 Some(mut precreated) => (
2293 precreated.isolate.take(),
2294 precreated.context.take(),
2295 true,
2296 Some(std::mem::take(&mut precreated.bridge_code)),
2297 Some(std::mem::take(&mut precreated.userland_code)),
2298 ),
2299 None => (None, None, false, None, None),
2300 };
2301
2302 #[cfg(not(test))]
2303 let mut pending = bridge::PendingPromises::new();
2304
2305 #[cfg(not(test))]
2307 let mut last_globals_payload: Option<Vec<u8>> = None;
2308
2309 #[cfg(not(test))]
2311 let mut bridge_cache: Option<execution::BridgeCodeCache> = None;
2312
2313 #[cfg(not(test))]
2315 let mut last_bridge_code: Option<String> = None;
2316 #[cfg(not(test))]
2318 let mut last_userland_code: Option<String> = None;
2319
2320 #[cfg(not(test))]
2326 let mut high_resolution_time_origin = Instant::now();
2327
2328 #[cfg(not(test))]
2329 if let Some(iso) = v8_isolate.as_mut() {
2330 *isolate_handle
2331 .lock()
2332 .expect("session isolate handle lock poisoned") = Some(iso.thread_safe_handle());
2333 }
2334
2335 loop {
2337 let next_command = recv_session_command(&rx, &shutdown_rx, &ready_rx, &ready_broker);
2338
2339 pause_control.wait_while_paused();
2340 match next_command {
2341 Some(SessionCommand::Shutdown) | None => break,
2342 Some(SessionCommand::ReadyBatch(batch)) => {
2343 if batch.signals_ready {
2344 if let Err(error) = ready_broker.drain_signals(&batch) {
2345 eprintln!("ERR_AGENTOS_READY_DISCARD: could not discard signals: {error}");
2346 }
2347 }
2348 if batch.timers_ready {
2349 if let Err(error) = ready_broker.drain_timers(&batch) {
2350 eprintln!("ERR_AGENTOS_READY_DISCARD: could not discard timers: {error}");
2351 }
2352 }
2353 if let Err(error) = ready_broker.complete_batch(&batch, &batch.entries) {
2354 eprintln!("{error}");
2355 }
2356 }
2357 Some(SessionCommand::SetModuleReader(reader)) => {
2358 execution::install_session_guest_reader(Some(reader));
2359 }
2360 Some(SessionCommand::Message(msg)) => match msg {
2361 SessionMessage::InjectGlobals { payload } => {
2362 #[cfg(not(test))]
2363 {
2364 last_globals_payload = Some(payload);
2366 }
2367 #[cfg(test)]
2368 {
2369 let _ = payload;
2370 }
2371 }
2372 SessionMessage::Execute {
2373 mode,
2374 file_path,
2375 bridge_code,
2376 post_restore_script,
2377 userland_code,
2378 high_resolution_time,
2379 user_code,
2380 wasm_module_bytes,
2381 } => {
2382 let _execution_activity = ExecutionActivityGuard(Arc::clone(&execution_active));
2383 #[cfg(test)]
2386 let _ = &userland_code;
2387 #[cfg(test)]
2388 let _ = high_resolution_time;
2389 #[cfg(test)]
2390 let _ = &wasm_module_bytes;
2391 #[cfg(not(test))]
2392 {
2393 let session_id = session_id.clone();
2394 let should_update_cached_bridge_code = !bridge_code.is_empty();
2396 let effective_bridge_code = if bridge_code.is_empty() {
2397 last_bridge_code.as_deref().unwrap_or("").to_string()
2398 } else {
2399 bridge_code
2400 };
2401 let should_update_cached_userland_code = !userland_code.is_empty();
2403 let effective_userland_code = if userland_code.is_empty() {
2404 last_userland_code.as_deref().unwrap_or("").to_string()
2405 } else {
2406 userland_code
2407 };
2408
2409 if let Err(message) =
2410 snapshot::validate_bridge_code_size(&effective_bridge_code)
2411 {
2412 let result_frame = RuntimeEvent::ExecutionResult {
2413 session_id,
2414 exit_code: 1,
2415 exports: None,
2416 error: Some(ExecutionErrorBin {
2417 error_type: "Error".into(),
2418 message,
2419 stack: String::new(),
2420 code: snapshot::V8_BRIDGE_CODE_LIMIT_ERROR_CODE.into(),
2421 }),
2422 };
2423 send_event_with_generation(&event_tx, output_generation, result_frame);
2424 continue;
2425 }
2426
2427 if should_update_cached_bridge_code {
2428 last_bridge_code = Some(effective_bridge_code.clone());
2429 }
2430 if should_update_cached_userland_code {
2431 last_userland_code = Some(effective_userland_code.clone());
2432 }
2433
2434 if v8_isolate.is_some()
2435 && (isolate_bridge_code.as_deref()
2436 != Some(effective_bridge_code.as_str())
2437 || isolate_userland_code.as_deref()
2438 != Some(effective_userland_code.as_str()))
2439 {
2440 *isolate_handle
2441 .lock()
2442 .expect("session isolate handle lock poisoned") = None;
2443 reset_pending_promises(&mut pending);
2450 drop(_v8_context.take());
2451 isolate::drop_isolate(v8_isolate.take());
2452 from_snapshot = false;
2453 isolate_bridge_code = None;
2454 isolate_userland_code = None;
2455 }
2456
2457 if v8_isolate.is_none() {
2459 isolate::init_v8_platform();
2460 let phase_start = Instant::now();
2464 let snapshot_blob = match snapshot_cache.get_or_create_with_userland(
2465 &effective_bridge_code,
2466 (!effective_userland_code.is_empty())
2467 .then_some(effective_userland_code.as_str()),
2468 ) {
2469 Ok(blob) => Some(blob),
2470 Err(message) => {
2471 eprintln!(
2476 "agentos-v8-runtime: snapshot creation failed, \
2477 falling back to fresh isolate: {message}"
2478 );
2479 None
2480 }
2481 };
2482 record_v8_session_phase("snapshot_get", phase_start.elapsed());
2483 let mut iso = match snapshot_blob {
2484 Some(blob) => {
2485 from_snapshot = true;
2486 eprintln!(
2487 "agentos-v8-runtime: restored session isolate from_snapshot=true"
2488 );
2489 let phase_start = Instant::now();
2490 let snapshot_blob = (*blob).clone();
2494 record_v8_session_phase("blob_clone", phase_start.elapsed());
2495 let phase_start = Instant::now();
2496 let isolate = snapshot::create_isolate_from_snapshot(
2497 snapshot_blob,
2498 heap_limit_mb,
2499 );
2500 record_v8_session_phase("isolate_new", phase_start.elapsed());
2501 isolate
2502 }
2503 None => {
2504 from_snapshot = false;
2505 let phase_start = Instant::now();
2506 let isolate = isolate::create_isolate(heap_limit_mb);
2507 record_v8_session_phase("isolate_new", phase_start.elapsed());
2508 isolate
2509 }
2510 };
2511 iso.set_host_import_module_dynamically_callback(
2512 execution::dynamic_import_callback,
2513 );
2514 iso.set_host_initialize_import_meta_object_callback(
2515 execution::import_meta_object_callback,
2516 );
2517 high_resolution_time_origin = Instant::now();
2518 *isolate_handle
2519 .lock()
2520 .expect("session isolate handle lock poisoned") =
2521 Some(iso.thread_safe_handle());
2522 let ctx = isolate::create_context(&mut iso);
2523 _v8_context = Some(ctx);
2524 v8_isolate = Some(iso);
2525 isolate_bridge_code = Some(effective_bridge_code.clone());
2526 isolate_userland_code = Some(effective_userland_code.clone());
2527 }
2528
2529 let iso = v8_isolate.as_mut().unwrap();
2530 iso.cancel_terminate_execution();
2531
2532 let retain_context = mode & 2 != 0;
2536 let module_mode = mode & 1 != 0;
2537 let exec_context = if retain_context {
2538 _v8_context
2539 .as_ref()
2540 .expect("session context exists after isolate creation")
2541 .clone()
2542 } else {
2543 isolate::create_context(iso)
2544 };
2545
2546 if high_resolution_time {
2547 let scope = &mut v8::HandleScope::new(iso);
2548 let ctx = v8::Local::new(scope, &exec_context);
2549 let scope = &mut v8::ContextScope::new(scope, ctx);
2550 execution::install_high_resolution_time_global(
2551 scope,
2552 &high_resolution_time_origin as *const Instant,
2553 );
2554 }
2555
2556 if let Some(ref payload) = last_globals_payload {
2558 let scope = &mut v8::HandleScope::new(iso);
2559 let ctx = v8::Local::new(scope, &exec_context);
2560 let scope = &mut v8::ContextScope::new(scope, ctx);
2561 if let Err(error) =
2562 execution::inject_globals_from_payload(scope, payload)
2563 {
2564 let result_frame = RuntimeEvent::ExecutionResult {
2565 session_id,
2566 exit_code: 1,
2567 exports: None,
2568 error: Some(ExecutionErrorBin {
2569 error_type: error.error_type,
2570 message: error.message,
2571 stack: error.stack,
2572 code: error.code.unwrap_or_default(),
2573 }),
2574 };
2575 send_event_with_generation(
2576 &event_tx,
2577 output_generation,
2578 result_frame,
2579 );
2580 continue;
2581 }
2582 }
2583
2584 let (_active_execution_abort, abort_rx) =
2587 ActiveExecutionAbort::arm(&execution_abort);
2588 if execution_abort_requested(&abort_rx) {
2589 send_event_with_generation(
2594 &event_tx,
2595 output_generation,
2596 RuntimeEvent::ExecutionResult {
2597 session_id,
2598 exit_code: 1,
2599 exports: None,
2600 error: Some(ExecutionErrorBin {
2601 error_type: String::from("Error"),
2602 message: String::from("Execution terminated"),
2603 stack: String::new(),
2604 code: String::new(),
2605 }),
2606 },
2607 );
2608 continue;
2609 }
2610
2611 let (async_response_tx, async_response_rx) =
2615 crossbeam_channel::bounded(bridge::MAX_PENDING_PROMISES);
2616 let bridge_ctx = BridgeCallContext::with_registry(
2617 Box::new(ChannelRuntimeEventSender::new(
2618 event_tx.clone(),
2619 output_generation,
2620 )),
2621 session_id.clone(),
2622 output_generation,
2623 Arc::clone(&call_id_router),
2624 Arc::clone(&shared_call_id),
2625 async_response_tx,
2626 abort_rx.clone(),
2627 runtime.clone(),
2628 Arc::clone(&pause_control),
2629 bridge_call_timeout,
2630 );
2631
2632 let _sync_store;
2636 let _async_store;
2637 let sync_bridge_fns = sync_bridge_fns();
2638 let async_bridge_fns = async_bridge_fns();
2639 {
2640 let scope = &mut v8::HandleScope::new(iso);
2641 let ctx = v8::Local::new(scope, &exec_context);
2642 let scope = &mut v8::ContextScope::new(scope, ctx);
2643
2644 (_sync_store, _async_store) = bridge::replace_bridge_fns(
2645 scope,
2646 &bridge_ctx as *const BridgeCallContext,
2647 &pending as *const bridge::PendingPromises,
2648 sync_bridge_fns,
2649 async_bridge_fns,
2650 );
2651 }
2652
2653 if !post_restore_script.is_empty() {
2656 let scope = &mut v8::HandleScope::new(iso);
2657 let ctx = v8::Local::new(scope, &exec_context);
2658 let scope = &mut v8::ContextScope::new(scope, ctx);
2659 let (prs_code, prs_err) =
2660 execution::run_init_script(scope, &post_restore_script);
2661 if prs_code != 0 {
2662 let result_frame = RuntimeEvent::ExecutionResult {
2663 session_id,
2664 exit_code: prs_code,
2665 exports: None,
2666 error: prs_err.map(|e| ExecutionErrorBin {
2667 error_type: e.error_type,
2668 message: e.message,
2669 stack: e.stack,
2670 code: e.code.unwrap_or_default(),
2671 }),
2672 };
2673 send_event_with_generation(
2674 &event_tx,
2675 output_generation,
2676 result_frame,
2677 );
2678 continue;
2679 }
2680 }
2681
2682 if let Some(wasm_module_bytes) = wasm_module_bytes.as_ref() {
2683 let scope = &mut v8::HandleScope::new(iso);
2684 let ctx = v8::Local::new(scope, &exec_context);
2685 let scope = &mut v8::ContextScope::new(scope, ctx);
2686 if !install_wasm_module_bytes_global(scope, wasm_module_bytes) {
2687 let result_frame = RuntimeEvent::ExecutionResult {
2688 session_id,
2689 exit_code: 1,
2690 exports: None,
2691 error: Some(ExecutionErrorBin {
2692 error_type: "Error".into(),
2693 message: "failed to install __agentOSWasmModuleBytes"
2694 .into(),
2695 stack: String::new(),
2696 code: String::new(),
2697 }),
2698 };
2699 send_event_with_generation(
2700 &event_tx,
2701 output_generation,
2702 result_frame,
2703 );
2704 continue;
2705 }
2706 }
2707
2708 let mut cpu_budget_guard = match cpu_time_limit_ms {
2720 Some(budget_ms) => {
2721 let cpu_clock = match exec_thread_cpu_clock {
2727 Some(clock) => clock,
2728 None => {
2729 let result_frame = RuntimeEvent::ExecutionResult {
2730 session_id,
2731 exit_code: 1,
2732 exports: None,
2733 error: Some(ExecutionErrorBin {
2734 error_type: "Error".into(),
2735 message: format!(
2736 "{}: per-thread CPU clock unavailable; cannot enforce limits.jsRuntime.cpuTimeLimitMs",
2737 crate::timeout::CPU_BUDGET_GUARD_START_ERROR_CODE
2738 ),
2739 stack: String::new(),
2740 code: crate::timeout::CPU_BUDGET_GUARD_START_ERROR_CODE
2741 .into(),
2742 }),
2743 };
2744 send_event_with_generation(
2745 &event_tx,
2746 output_generation,
2747 result_frame,
2748 );
2749 continue;
2750 }
2751 };
2752 let handle = iso.thread_safe_handle();
2753 match crate::timeout::CpuBudgetGuard::new(
2754 &runtime,
2755 execution_task_owner.clone(),
2756 budget_ms,
2757 cpu_clock,
2758 handle,
2759 execution_abort.clone(),
2760 ) {
2761 Ok(guard) => Some(guard),
2762 Err(message) => {
2763 let result_frame = RuntimeEvent::ExecutionResult {
2764 session_id,
2765 exit_code: 1,
2766 exports: None,
2767 error: Some(ExecutionErrorBin {
2768 error_type: "Error".into(),
2769 message,
2770 stack: String::new(),
2771 code:
2772 crate::timeout::CPU_BUDGET_GUARD_START_ERROR_CODE
2773 .into(),
2774 }),
2775 };
2776 send_event_with_generation(
2777 &event_tx,
2778 output_generation,
2779 result_frame,
2780 );
2781 continue;
2782 }
2783 }
2784 }
2785 _ => None,
2786 };
2787
2788 let mut wall_clock_guard = match wall_clock_limit_ms {
2798 Some(limit_ms) => {
2799 let handle = iso.thread_safe_handle();
2800 match crate::timeout::TimeoutGuard::with_execution_abort(
2801 &runtime,
2802 execution_task_owner.clone(),
2803 limit_ms,
2804 handle,
2805 execution_abort.clone(),
2806 ) {
2807 Ok(guard) => Some(guard),
2808 Err(message) => {
2809 let result_frame = RuntimeEvent::ExecutionResult {
2810 session_id,
2811 exit_code: 1,
2812 exports: None,
2813 error: Some(ExecutionErrorBin {
2814 error_type: "Error".into(),
2815 message,
2816 stack: String::new(),
2817 code:
2818 crate::timeout::TIMEOUT_GUARD_START_ERROR_CODE
2819 .into(),
2820 }),
2821 };
2822 send_event_with_generation(
2823 &event_tx,
2824 output_generation,
2825 result_frame,
2826 );
2827 continue;
2828 }
2829 }
2830 }
2831 _ => None,
2832 };
2833
2834 let bridge_code_for_exec = if from_snapshot {
2838 ""
2839 } else {
2840 &effective_bridge_code
2841 };
2842 let file_path_opt = if file_path.is_empty() {
2843 None
2844 } else {
2845 Some(file_path.as_str())
2846 };
2847 let phase_start = Instant::now();
2848 let (mut code, mut exports, mut error) = if !module_mode {
2849 let scope = &mut v8::HandleScope::new(iso);
2850 let ctx = v8::Local::new(scope, &exec_context);
2851 let scope = &mut v8::ContextScope::new(scope, ctx);
2852 let (c, e) = execution::execute_script_with_options(
2853 scope,
2854 Some(&bridge_ctx),
2855 bridge_code_for_exec,
2856 &user_code,
2857 file_path_opt,
2858 &mut bridge_cache,
2859 );
2860 (c, None, e)
2861 } else {
2862 let scope = &mut v8::HandleScope::new(iso);
2863 let ctx = v8::Local::new(scope, &exec_context);
2864 let scope = &mut v8::ContextScope::new(scope, ctx);
2865 execution::execute_module(
2866 scope,
2867 &bridge_ctx,
2868 bridge_code_for_exec,
2869 &user_code,
2870 file_path_opt,
2871 &mut bridge_cache,
2872 )
2873 };
2874
2875 if module_mode && error.is_none() {
2879 let scope = &mut v8::HandleScope::new(iso);
2880 let ctx = v8::Local::new(scope, &exec_context);
2881 let scope = &mut v8::ContextScope::new(scope, ctx);
2882 if let Some((next_code, next_exports, next_error)) =
2883 execution::finalize_pending_module_evaluation(scope)
2884 {
2885 code = next_code;
2886 exports = next_exports;
2887 error = next_error;
2888 }
2889 }
2890 record_v8_session_phase("user_code_execute", phase_start.elapsed());
2891
2892 let should_enter_event_loop = !pending.is_empty()
2899 || execution::has_pending_module_evaluation()
2900 || execution::has_pending_script_evaluation();
2901 let event_loop_status = if should_enter_event_loop {
2902 let scope = &mut v8::HandleScope::new(iso);
2903 let ctx = v8::Local::new(scope, &exec_context);
2904 let scope = &mut v8::ContextScope::new(scope, ctx);
2905 run_event_loop_with_readiness(
2906 scope,
2907 EventLoopSources {
2908 commands: &rx,
2909 readiness: &ready_broker,
2910 readiness_wakes: &ready_rx,
2911 bridge_responses: Some(&async_response_rx),
2912 abort: Some(&abort_rx),
2913 pause: Some(&pause_control),
2914 },
2915 &pending,
2916 )
2917 } else {
2918 EventLoopStatus::Completed
2919 };
2920
2921 let mut terminated =
2922 matches!(event_loop_status, EventLoopStatus::Terminated);
2923 if let EventLoopStatus::Failed(next_code, next_error) = event_loop_status {
2924 code = next_code;
2925 error = Some(next_error);
2926 }
2927
2928 if !terminated && module_mode && error.is_none() {
2931 let scope = &mut v8::HandleScope::new(iso);
2932 let ctx = v8::Local::new(scope, &exec_context);
2933 let scope = &mut v8::ContextScope::new(scope, ctx);
2934 if let Some((next_code, next_exports, next_error)) =
2935 execution::finalize_pending_module_evaluation(scope)
2936 {
2937 code = next_code;
2938 exports = next_exports;
2939 error = next_error;
2940 }
2941 }
2942
2943 if !terminated && error.is_none() {
2948 if execution_abort_requested(&abort_rx) {
2952 terminated = true;
2953 } else {
2954 {
2959 let scope = &mut v8::HandleScope::new(iso);
2960 let ctx = v8::Local::new(scope, &exec_context);
2961 let scope = &mut v8::ContextScope::new(scope, ctx);
2962 let global = ctx.global(scope);
2963 let key =
2964 v8::String::new(scope, "_waitForActiveHandles").unwrap();
2965 if let Some(func) = global.get(scope, key.into()) {
2966 if func.is_function() {
2967 let func =
2968 v8::Local::<v8::Function>::try_from(func).unwrap();
2969 let recv = v8::undefined(scope).into();
2970 if let Some(result) = func.call(scope, recv, &[]) {
2971 if result.is_promise() {
2972 let promise =
2973 v8::Local::<v8::Promise>::try_from(result)
2974 .unwrap();
2975 if promise.state() == v8::PromiseState::Pending
2976 {
2977 execution::set_pending_script_evaluation(
2978 scope, promise,
2979 );
2980 }
2981 }
2982 }
2983 }
2984 }
2985 }
2986
2987 if !pending.is_empty() || execution::has_pending_script_evaluation()
2989 {
2990 let scope = &mut v8::HandleScope::new(iso);
2991 let ctx = v8::Local::new(scope, &exec_context);
2992 let scope = &mut v8::ContextScope::new(scope, ctx);
2993 let event_loop_status = run_event_loop_with_readiness(
2994 scope,
2995 EventLoopSources {
2996 commands: &rx,
2997 readiness: &ready_broker,
2998 readiness_wakes: &ready_rx,
2999 bridge_responses: Some(&async_response_rx),
3000 abort: Some(&abort_rx),
3001 pause: Some(&pause_control),
3002 },
3003 &pending,
3004 );
3005
3006 if matches!(event_loop_status, EventLoopStatus::Terminated) {
3007 terminated = true;
3008 }
3009 if let EventLoopStatus::Failed(next_code, next_error) =
3010 event_loop_status
3011 {
3012 code = next_code;
3013 error = Some(next_error);
3014 }
3015 }
3016 }
3017 }
3018
3019 if !terminated && !module_mode && error.is_none() {
3020 let scope = &mut v8::HandleScope::new(iso);
3021 let ctx = v8::Local::new(scope, &exec_context);
3022 let scope = &mut v8::ContextScope::new(scope, ctx);
3023 if let Some((next_code, next_error)) =
3024 execution::finalize_pending_script_evaluation(scope)
3025 {
3026 code = next_code;
3027 error = next_error;
3028 }
3029 }
3030
3031 if !terminated && error.is_none() {
3036 let scope = &mut v8::HandleScope::new(iso);
3037 let ctx = v8::Local::new(scope, &exec_context);
3038 let scope = &mut v8::ContextScope::new(scope, ctx);
3039 if let Some(process_exit_code) =
3040 execution::extract_global_process_exit_code(scope)
3041 {
3042 code = process_exit_code;
3043 }
3044 }
3045
3046 let abort_reason = execution_abort_reason(&execution_abort);
3052 let wall_clock_timed_out =
3053 wall_clock_guard.as_ref().is_some_and(|g| g.timed_out())
3054 || matches!(
3055 abort_reason,
3056 Some(ExecutionAbortReason::WallClockTimedOut)
3057 );
3058 let cpu_budget_exceeded =
3059 cpu_budget_guard.as_ref().is_some_and(|g| g.exceeded())
3060 || matches!(
3061 abort_reason,
3062 Some(ExecutionAbortReason::CpuBudgetExceeded)
3063 );
3064 let cpu_budget_exceeded = cpu_budget_exceeded
3068 && !matches!(
3069 abort_reason,
3070 Some(ExecutionAbortReason::WallClockTimedOut)
3071 );
3072 let wall_clock_timed_out = wall_clock_timed_out && !cpu_budget_exceeded;
3073
3074 if let Some(ref mut guard) = cpu_budget_guard {
3076 guard.cancel();
3077 }
3078 drop(cpu_budget_guard);
3079 if let Some(ref mut guard) = wall_clock_guard {
3080 guard.cancel();
3081 }
3082 drop(wall_clock_guard);
3083
3084 if matches!(abort_reason, Some(ExecutionAbortReason::Terminated)) {
3085 terminated = true;
3086 code = 1;
3087 exports = None;
3088 error = None;
3089 }
3090 if terminated || cpu_budget_exceeded || wall_clock_timed_out {
3091 iso.cancel_terminate_execution();
3092 }
3093
3094 let result_frame = if cpu_budget_exceeded {
3096 if let Some(budget_ms) = cpu_time_limit_ms {
3097 let capacity = budget_ms as usize;
3098 warn_limit_exhausted(TrackedLimit::V8CpuTimeMs, capacity, capacity);
3099 }
3100 RuntimeEvent::ExecutionResult {
3101 session_id,
3102 exit_code: 1,
3103 exports: None,
3104 error: Some(ExecutionErrorBin {
3105 error_type: "Error".into(),
3106 message: "Script execution exceeded the CPU-time budget \
3107 (limits.jsRuntime.cpuTimeLimitMs)"
3108 .into(),
3109 stack: String::new(),
3110 code: "ERR_SCRIPT_CPU_BUDGET_EXCEEDED".into(),
3111 }),
3112 }
3113 } else if wall_clock_timed_out {
3114 if let Some(limit_ms) = wall_clock_limit_ms {
3115 let capacity = limit_ms as usize;
3116 warn_limit_exhausted(
3117 TrackedLimit::V8WallClockMs,
3118 capacity,
3119 capacity,
3120 );
3121 }
3122 RuntimeEvent::ExecutionResult {
3123 session_id,
3124 exit_code: 1,
3125 exports: None,
3126 error: Some(ExecutionErrorBin {
3127 error_type: "Error".into(),
3128 message: "Script execution exceeded the wall-clock limit \
3129 (limits.jsRuntime.wallClockLimitMs)"
3130 .into(),
3131 stack: String::new(),
3132 code: "ERR_SCRIPT_WALL_CLOCK_EXCEEDED".into(),
3133 }),
3134 }
3135 } else if terminated {
3136 RuntimeEvent::ExecutionResult {
3137 session_id,
3138 exit_code: 1,
3139 exports: None,
3140 error: Some(ExecutionErrorBin {
3141 error_type: "Error".into(),
3142 message: "Execution terminated".into(),
3143 stack: String::new(),
3144 code: String::new(),
3145 }),
3146 }
3147 } else {
3148 RuntimeEvent::ExecutionResult {
3149 session_id,
3150 exit_code: code,
3151 exports,
3152 error: error.map(|e| ExecutionErrorBin {
3153 error_type: e.error_type,
3154 message: e.message,
3155 stack: e.stack,
3156 code: e.code.unwrap_or_default(),
3157 }),
3158 }
3159 };
3160
3161 execution::clear_pending_module_evaluation();
3162 execution::clear_pending_script_evaluation();
3163 execution::clear_module_state();
3164
3165 send_event_with_generation(&event_tx, output_generation, result_frame);
3166 }
3167 #[cfg(test)]
3168 {
3169 let _ = (mode, file_path, bridge_code, post_restore_script, user_code);
3170 }
3171 }
3172 SessionMessage::BridgeResponse(_)
3173 | SessionMessage::StreamEvent(_)
3174 | SessionMessage::TerminateExecution => {
3175 handle_late_session_message(&event_tx, &session_id, output_generation, msg);
3176 }
3177 },
3178 }
3179 }
3180
3181 #[cfg(not(test))]
3183 {
3184 *isolate_handle
3185 .lock()
3186 .expect("session isolate handle lock poisoned") = None;
3187 reset_pending_promises(&mut pending);
3193 drop(_v8_context.take());
3194 isolate::drop_isolate(v8_isolate.take());
3195 }
3196
3197 }
3200
3201struct BridgeFnPartitions {
3204 sync: Vec<&'static str>,
3205 async_fns: Vec<&'static str>,
3206}
3207
3208pub(crate) fn sync_bridge_fns() -> &'static [&'static str] {
3209 &bridge_fn_partitions().sync
3210}
3211
3212pub(crate) fn async_bridge_fns() -> &'static [&'static str] {
3213 &bridge_fn_partitions().async_fns
3214}
3215
3216fn bridge_fn_partitions() -> &'static BridgeFnPartitions {
3217 static PARTITIONS: OnceLock<BridgeFnPartitions> = OnceLock::new();
3218 PARTITIONS.get_or_init(|| BridgeFnPartitions {
3219 sync: bridge_fns_for(|convention| {
3220 matches!(
3221 convention,
3222 BridgeCallConvention::Sync | BridgeCallConvention::SyncPromise
3223 )
3224 }),
3225 async_fns: bridge_fns_for(|convention| convention == BridgeCallConvention::Async),
3226 })
3227}
3228
3229fn bridge_fns_for(filter: impl Fn(BridgeCallConvention) -> bool) -> Vec<&'static str> {
3230 bridge_contract()
3231 .groups
3232 .iter()
3233 .filter(|group| filter(group.convention))
3234 .flat_map(|group| group.names.iter().map(String::as_str))
3235 .collect()
3236}
3237
3238#[doc(hidden)]
3251pub fn reset_pending_promises(pending: &mut crate::bridge::PendingPromises) {
3252 drop(std::mem::take(pending));
3255}
3256
3257#[doc(hidden)]
3269pub fn run_event_loop(
3270 scope: &mut v8::HandleScope,
3271 rx: &Receiver<SessionCommand>,
3272 pending: &crate::bridge::PendingPromises,
3273 abort_rx: Option<&crossbeam_channel::Receiver<()>>,
3274 bridge_rx: Option<&crossbeam_channel::Receiver<BridgeResponse>>,
3275 pause_control: Option<&SessionPauseControl>,
3276) -> EventLoopStatus {
3277 let (ready_broker, ready_rx) = match SessionReadiness::disabled(1) {
3278 Ok(readiness) => readiness,
3279 Err(error) => {
3280 eprintln!("{error}");
3281 return EventLoopStatus::Terminated;
3282 }
3283 };
3284 run_event_loop_with_readiness(
3285 scope,
3286 EventLoopSources {
3287 commands: rx,
3288 readiness: &ready_broker,
3289 readiness_wakes: &ready_rx,
3290 bridge_responses: bridge_rx,
3291 abort: abort_rx,
3292 pause: pause_control,
3293 },
3294 pending,
3295 )
3296}
3297
3298struct EventLoopSources<'a> {
3299 commands: &'a Receiver<SessionCommand>,
3300 readiness: &'a SessionReadiness,
3301 readiness_wakes: &'a Receiver<ReadyWake>,
3302 bridge_responses: Option<&'a Receiver<BridgeResponse>>,
3303 abort: Option<&'a Receiver<()>>,
3304 pause: Option<&'a SessionPauseControl>,
3305}
3306
3307fn run_event_loop_with_readiness(
3308 scope: &mut v8::HandleScope,
3309 sources: EventLoopSources<'_>,
3310 pending: &crate::bridge::PendingPromises,
3311) -> EventLoopStatus {
3312 let EventLoopSources {
3313 commands: rx,
3314 readiness: ready_broker,
3315 readiness_wakes: ready_rx,
3316 bridge_responses: bridge_rx,
3317 abort: abort_rx,
3318 pause: pause_control,
3319 } = sources;
3320 let mut bridge_lane_open = bridge_rx.is_some();
3321 loop {
3322 if abort_rx.is_some_and(execution_abort_requested) {
3327 scope.terminate_execution();
3328 return EventLoopStatus::Terminated;
3329 }
3330 if pending.is_empty()
3331 && !execution::pending_module_evaluation_needs_wait(scope)
3332 && !execution::pending_script_evaluation_needs_wait(scope)
3333 && pending_guest_timer_count(scope) == 0
3334 && pending_guest_immediate_count(scope) == 0
3335 {
3336 break;
3337 }
3338 if let Some(control) = pause_control {
3339 control.wait_while_paused();
3340 }
3341 pump_v8_message_loop(scope);
3342
3343 if bridge_lane_open {
3346 let responses = bridge_rx.expect("open bridge lane must have a receiver");
3347 for _ in 0..64 {
3348 let response = match responses.try_recv() {
3349 Ok(response) => response,
3350 Err(crossbeam_channel::TryRecvError::Empty) => break,
3351 Err(crossbeam_channel::TryRecvError::Disconnected) => {
3352 bridge_lane_open = false;
3353 break;
3354 }
3355 };
3356 let status = dispatch_event_loop_frame(
3357 scope,
3358 SessionMessage::BridgeResponse(response),
3359 pending,
3360 );
3361 if !matches!(status, EventLoopStatus::Completed) {
3362 return status;
3363 }
3364 }
3365 }
3366
3367 scope.perform_microtask_checkpoint();
3373 pump_v8_message_loop(scope);
3374
3375 if pending_guest_immediate_count(scope) > 0 {
3376 match try_recv_session_command(scope, rx, ready_rx, ready_broker, bridge_rx, abort_rx) {
3377 Ok(Some(cmd)) => {
3378 let status = dispatch_session_command(scope, cmd, pending, ready_broker);
3379 if !matches!(status, EventLoopStatus::Completed) {
3380 return status;
3381 }
3382 }
3383 Ok(None) => {
3384 let status = drain_guest_immediates(scope);
3385 if !matches!(status, EventLoopStatus::Completed) {
3386 return status;
3387 }
3388 }
3389 Err(status) => return status,
3390 }
3391 scope.perform_microtask_checkpoint();
3392 pump_v8_message_loop(scope);
3393 }
3394
3395 if pending.is_empty()
3398 && !execution::pending_module_evaluation_needs_wait(scope)
3399 && !execution::pending_script_evaluation_needs_wait(scope)
3400 && pending_guest_timer_count(scope) == 0
3401 && pending_guest_immediate_count(scope) == 0
3402 {
3403 break;
3404 }
3405
3406 let cmd = loop {
3410 if pending_guest_immediate_count(scope) > 0 {
3411 match try_recv_session_command(
3412 scope,
3413 rx,
3414 ready_rx,
3415 ready_broker,
3416 bridge_rx,
3417 abort_rx,
3418 ) {
3419 Ok(Some(cmd)) => break cmd,
3420 Ok(None) => {
3421 let status = drain_guest_immediates(scope);
3422 if !matches!(status, EventLoopStatus::Completed) {
3423 return status;
3424 }
3425 scope.perform_microtask_checkpoint();
3426 pump_v8_message_loop(scope);
3427 continue;
3428 }
3429 Err(status) => return status,
3430 }
3431 }
3432 if bridge_lane_open {
3433 let responses = bridge_rx.expect("open bridge lane must have a receiver");
3434 match responses.try_recv() {
3435 Ok(response) => {
3436 break SessionCommand::Message(SessionMessage::BridgeResponse(response));
3437 }
3438 Err(crossbeam_channel::TryRecvError::Empty) => {}
3439 Err(crossbeam_channel::TryRecvError::Disconnected) => {
3440 bridge_lane_open = false;
3441 }
3442 }
3443 }
3444 match rx.try_recv() {
3449 Ok(command) => break command,
3450 Err(crossbeam_channel::TryRecvError::Empty) => {}
3451 Err(crossbeam_channel::TryRecvError::Disconnected) => {
3452 return EventLoopStatus::Completed;
3453 }
3454 }
3455 if let Ok(wake) = ready_rx.try_recv() {
3456 match ready_batch_command(ready_broker, wake) {
3457 Ok(command) => break command,
3458 Err(error) => {
3459 eprintln!("{error}");
3460 continue;
3461 }
3462 }
3463 }
3464 let mut selector = Select::new();
3469 let ordinary_index = selector.recv(rx);
3470 let ready_index = selector.recv(ready_rx);
3471 let bridge_selection = if bridge_lane_open {
3472 bridge_rx.map(|responses| (selector.recv(responses), responses))
3473 } else {
3474 None
3475 };
3476 let abort_selection = abort_rx.map(|abort| (selector.recv(abort), abort));
3477 let recv_result = match selector.select_timeout(Duration::from_millis(1)) {
3478 Ok(operation) => {
3479 let index = operation.index();
3480 if index == ordinary_index {
3481 operation.recv(rx).ok()
3482 } else if index == ready_index {
3483 match operation.recv(ready_rx) {
3484 Ok(wake) => match ready_batch_command(ready_broker, wake) {
3485 Ok(command) => Some(command),
3486 Err(error) => {
3487 eprintln!("{error}");
3488 None
3489 }
3490 },
3491 Err(_) => None,
3492 }
3493 } else if let Some((bridge_index, responses)) = bridge_selection {
3494 if index == bridge_index {
3495 match operation.recv(responses) {
3496 Ok(response) => Some(SessionCommand::Message(
3497 SessionMessage::BridgeResponse(response),
3498 )),
3499 Err(_) => {
3500 bridge_lane_open = false;
3501 None
3502 }
3503 }
3504 } else if let Some((abort_index, abort)) = abort_selection {
3505 debug_assert_eq!(index, abort_index);
3506 let _ = operation.recv(abort);
3507 scope.terminate_execution();
3508 return EventLoopStatus::Terminated;
3509 } else {
3510 unreachable!("event-loop selector returned an unknown operation")
3511 }
3512 } else if let Some((abort_index, abort)) = abort_selection {
3513 debug_assert_eq!(index, abort_index);
3514 let _ = operation.recv(abort);
3515 scope.terminate_execution();
3516 return EventLoopStatus::Terminated;
3517 } else {
3518 unreachable!("event-loop selector returned an unknown operation")
3519 }
3520 }
3521 Err(_) => None,
3522 };
3523 if let Some(cmd) = recv_result {
3524 break cmd;
3525 }
3526 if let Some(control) = pause_control {
3527 control.wait_while_paused();
3528 }
3529 scope.perform_microtask_checkpoint();
3532 pump_v8_message_loop(scope);
3533 if pending.is_empty()
3535 && !execution::pending_module_evaluation_needs_wait(scope)
3536 && !execution::pending_script_evaluation_needs_wait(scope)
3537 && pending_guest_timer_count(scope) == 0
3538 && pending_guest_immediate_count(scope) == 0
3539 {
3540 return EventLoopStatus::Completed;
3541 }
3542 };
3543
3544 let status = dispatch_session_command(scope, cmd, pending, ready_broker);
3545 if !matches!(status, EventLoopStatus::Completed) {
3546 return status;
3547 }
3548 }
3549 EventLoopStatus::Completed
3550}
3551
3552fn execution_abort_requested(abort: &crossbeam_channel::Receiver<()>) -> bool {
3553 !matches!(
3554 abort.try_recv(),
3555 Err(crossbeam_channel::TryRecvError::Empty)
3556 )
3557}
3558
3559fn try_recv_session_command(
3560 scope: &mut v8::HandleScope,
3561 rx: &Receiver<SessionCommand>,
3562 ready_rx: &Receiver<ReadyWake>,
3563 ready_broker: &SessionReadiness,
3564 bridge_rx: Option<&crossbeam_channel::Receiver<BridgeResponse>>,
3565 abort_rx: Option<&crossbeam_channel::Receiver<()>>,
3566) -> Result<Option<SessionCommand>, EventLoopStatus> {
3567 if let Some(responses) = bridge_rx {
3568 match responses.try_recv() {
3569 Ok(response) => {
3570 return Ok(Some(SessionCommand::Message(
3571 SessionMessage::BridgeResponse(response),
3572 )));
3573 }
3574 Err(crossbeam_channel::TryRecvError::Empty) => {}
3575 Err(crossbeam_channel::TryRecvError::Disconnected) => {}
3576 }
3577 }
3578 if let Some(abort) = abort_rx {
3579 match abort.try_recv() {
3580 Ok(()) | Err(crossbeam_channel::TryRecvError::Disconnected) => {
3581 scope.terminate_execution();
3582 return Err(EventLoopStatus::Terminated);
3583 }
3584 Err(crossbeam_channel::TryRecvError::Empty) => {}
3585 }
3586 }
3587 match rx.try_recv() {
3588 Ok(command) => return Ok(Some(command)),
3589 Err(crossbeam_channel::TryRecvError::Empty) => {}
3590 Err(crossbeam_channel::TryRecvError::Disconnected) => return Ok(None),
3591 }
3592 match ready_rx.try_recv() {
3593 Ok(wake) => match ready_batch_command(ready_broker, wake) {
3594 Ok(command) => return Ok(Some(command)),
3595 Err(error) => {
3596 eprintln!("{error}");
3597 }
3598 },
3599 Err(crossbeam_channel::TryRecvError::Empty) => {}
3600 Err(crossbeam_channel::TryRecvError::Disconnected) => {}
3601 }
3602 if let Some(abort) = abort_rx {
3603 crossbeam_channel::select! {
3604 recv(abort) -> _ => {
3605 scope.terminate_execution();
3606 Err(EventLoopStatus::Terminated)
3607 },
3608 recv(rx) -> result => Ok(result.ok()),
3609 default => Ok(None),
3610 }
3611 } else {
3612 match rx.try_recv() {
3613 Ok(cmd) => Ok(Some(cmd)),
3614 Err(crossbeam_channel::TryRecvError::Empty) => Ok(None),
3615 Err(crossbeam_channel::TryRecvError::Disconnected) => Ok(None),
3616 }
3617 }
3618}
3619
3620fn dispatch_session_command(
3621 scope: &mut v8::HandleScope,
3622 cmd: SessionCommand,
3623 pending: &crate::bridge::PendingPromises,
3624 ready_broker: &SessionReadiness,
3625) -> EventLoopStatus {
3626 match cmd {
3627 SessionCommand::Message(frame) => dispatch_event_loop_frame(scope, frame, pending),
3628 SessionCommand::ReadyBatch(batch) => dispatch_ready_batch(scope, batch, ready_broker),
3629 SessionCommand::SetModuleReader(reader) => {
3630 execution::install_session_guest_reader(Some(reader));
3631 EventLoopStatus::Completed
3632 }
3633 SessionCommand::Shutdown => EventLoopStatus::Terminated,
3634 }
3635}
3636
3637fn dispatch_ready_batch(
3645 scope: &mut v8::HandleScope,
3646 batch: RuntimeReadyBatch,
3647 ready_broker: &SessionReadiness,
3648) -> EventLoopStatus {
3649 let mut delivered = Vec::with_capacity(batch.entries.len());
3650 let dispatch_status =
3651 dispatch_ready_batch_callbacks(scope, &batch, ready_broker, &mut delivered);
3652 complete_ready_batch_dispatch(ready_broker, &batch, &delivered, dispatch_status)
3653}
3654
3655fn dispatch_ready_batch_callbacks(
3656 scope: &mut v8::HandleScope,
3657 batch: &RuntimeReadyBatch,
3658 ready_broker: &SessionReadiness,
3659 delivered: &mut Vec<ReadyObservation>,
3660) -> EventLoopStatus {
3661 for entry in &batch.entries {
3662 let tc = &mut v8::TryCatch::new(scope);
3663 let dispatch = crate::stream::dispatch_readiness(
3664 tc,
3665 entry.capability_id,
3666 entry.capability_generation,
3667 entry.flags,
3668 );
3669 tc.perform_microtask_checkpoint();
3670 if let Some(exception) = tc.exception() {
3671 let (code, error) = execution::exception_to_result(tc, exception);
3672 return EventLoopStatus::Failed(code, error);
3673 }
3674 match dispatch {
3675 crate::stream::ReadinessDispatch::Delivered => delivered.push(*entry),
3676 crate::stream::ReadinessDispatch::TargetMissing => {
3677 }
3682 crate::stream::ReadinessDispatch::BridgeMissing => {
3683 return EventLoopStatus::Failed(
3684 1,
3685 ExecutionError {
3686 error_type: String::from("Error"),
3687 message: String::from(
3688 "ERR_AGENTOS_READY_DISPATCH_MISSING: guest bridge does not expose _agentOSReadyDispatch",
3689 ),
3690 stack: String::new(),
3691 code: Some(String::from("ERR_AGENTOS_READY_DISPATCH_MISSING")),
3692 },
3693 );
3694 }
3695 }
3696 }
3697 if batch.signals_ready {
3698 let signals = match ready_broker.drain_signals(batch) {
3699 Ok(signals) => signals,
3700 Err(error) => return readiness_dispatch_failure(error),
3701 };
3702 for signal in signals {
3703 let Some(signal_name) = signal_name_for_stream_event(signal) else {
3704 continue;
3705 };
3706 let tc = &mut v8::TryCatch::new(scope);
3707 crate::stream::dispatch_signal_event(tc, signal_name, signal);
3708 tc.perform_microtask_checkpoint();
3709 if let Some(exception) = tc.exception() {
3710 let (code, error) = execution::exception_to_result(tc, exception);
3711 return EventLoopStatus::Failed(code, error);
3712 }
3713 if let Some(error) = execution::take_unhandled_promise_rejection(tc) {
3714 return EventLoopStatus::Failed(1, error);
3715 }
3716 }
3717 }
3718 if batch.timers_ready {
3719 let timers = match ready_broker.drain_timers(batch) {
3720 Ok(timers) => timers,
3721 Err(error) => return readiness_dispatch_failure(error),
3722 };
3723 for timer_id in timers {
3724 let tc = &mut v8::TryCatch::new(scope);
3725 crate::stream::dispatch_timer_event(tc, timer_id);
3726 tc.perform_microtask_checkpoint();
3727 if let Some(exception) = tc.exception() {
3728 let (code, error) = execution::exception_to_result(tc, exception);
3729 return EventLoopStatus::Failed(code, error);
3730 }
3731 if let Some(error) = execution::take_unhandled_promise_rejection(tc) {
3732 return EventLoopStatus::Failed(1, error);
3733 }
3734 }
3735 }
3736 EventLoopStatus::Completed
3737}
3738
3739fn complete_ready_batch_dispatch(
3740 ready_broker: &SessionReadiness,
3741 batch: &RuntimeReadyBatch,
3742 delivered: &[ReadyObservation],
3743 dispatch_status: EventLoopStatus,
3744) -> EventLoopStatus {
3745 if let Err(error) = ready_broker.complete_batch(batch, delivered) {
3746 if matches!(&dispatch_status, EventLoopStatus::Completed) {
3747 return readiness_dispatch_failure(error);
3748 }
3749 eprintln!(
3750 "ERR_AGENTOS_READY_COMPLETE_AFTER_DISPATCH_FAILURE: could not complete readiness wake after guest dispatch failed: {error}"
3751 );
3752 }
3753 dispatch_status
3754}
3755
3756fn readiness_dispatch_failure(message: String) -> EventLoopStatus {
3757 EventLoopStatus::Failed(
3758 1,
3759 ExecutionError {
3760 error_type: String::from("Error"),
3761 message,
3762 stack: String::new(),
3763 code: Some(String::from("ERR_AGENTOS_READY_COMPLETE")),
3764 },
3765 )
3766}
3767
3768fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
3769 match signal {
3770 1 => Some("SIGHUP"),
3771 2 => Some("SIGINT"),
3772 10 => Some("SIGUSR1"),
3773 14 => Some("SIGALRM"),
3774 18 => Some("SIGCONT"),
3775 15 => Some("SIGTERM"),
3776 17 => Some("SIGCHLD"),
3777 28 => Some("SIGWINCH"),
3778 _ => None,
3779 }
3780}
3781
3782fn pending_guest_timer_count(scope: &mut v8::HandleScope) -> usize {
3783 let tc = &mut v8::TryCatch::new(scope);
3784 let context = tc.get_current_context();
3785 let global = context.global(tc);
3786 let key = match v8::String::new(tc, "_getPendingTimerCount") {
3787 Some(key) => key,
3788 None => return 0,
3789 };
3790 let Some(func_value) = global.get(tc, key.into()) else {
3791 return 0;
3792 };
3793 let Ok(func) = v8::Local::<v8::Function>::try_from(func_value) else {
3794 return 0;
3795 };
3796 let Some(result) = func.call(tc, global.into(), &[]) else {
3797 return 0;
3798 };
3799
3800 result
3801 .integer_value(tc)
3802 .and_then(|count| usize::try_from(count).ok())
3803 .unwrap_or(0)
3804}
3805
3806fn pending_guest_immediate_count(scope: &mut v8::HandleScope) -> usize {
3807 let tc = &mut v8::TryCatch::new(scope);
3808 let context = tc.get_current_context();
3809 let global = context.global(tc);
3810 let key = match v8::String::new(tc, "_getPendingImmediateCount") {
3811 Some(key) => key,
3812 None => return 0,
3813 };
3814 let Some(func_value) = global.get(tc, key.into()) else {
3815 return 0;
3816 };
3817 let Ok(func) = v8::Local::<v8::Function>::try_from(func_value) else {
3818 return 0;
3819 };
3820 let Some(result) = func.call(tc, global.into(), &[]) else {
3821 return 0;
3822 };
3823
3824 result
3825 .integer_value(tc)
3826 .and_then(|count| usize::try_from(count).ok())
3827 .unwrap_or(0)
3828}
3829
3830fn drain_guest_immediates(scope: &mut v8::HandleScope) -> EventLoopStatus {
3831 let tc = &mut v8::TryCatch::new(scope);
3832 let context = tc.get_current_context();
3833 let global = context.global(tc);
3834 let key = match v8::String::new(tc, "_drainImmediates") {
3835 Some(key) => key,
3836 None => return EventLoopStatus::Completed,
3837 };
3838 let Some(func_value) = global.get(tc, key.into()) else {
3839 return EventLoopStatus::Completed;
3840 };
3841 let Ok(func) = v8::Local::<v8::Function>::try_from(func_value) else {
3842 return EventLoopStatus::Completed;
3843 };
3844 let _ = func.call(tc, global.into(), &[]);
3845 tc.perform_microtask_checkpoint();
3846 if let Some(exception) = tc.exception() {
3847 let (code, err) = execution::exception_to_result(tc, exception);
3848 return EventLoopStatus::Failed(code, err);
3849 }
3850 if let Some(err) = execution::take_unhandled_promise_rejection(tc) {
3851 return EventLoopStatus::Failed(1, err);
3852 }
3853 EventLoopStatus::Completed
3854}
3855
3856fn pump_v8_message_loop(scope: &mut v8::HandleScope) {
3857 let platform = v8::V8::get_current_platform();
3858 while v8::Platform::pump_message_loop(&platform, scope, false) {
3859 scope.perform_microtask_checkpoint();
3860 }
3861}
3862
3863#[derive(Debug)]
3866#[doc(hidden)]
3867pub enum EventLoopStatus {
3868 Completed,
3869 Terminated,
3870 Failed(i32, ExecutionError),
3871}
3872
3873fn dispatch_event_loop_frame(
3874 scope: &mut v8::HandleScope,
3875 frame: SessionMessage,
3876 pending: &crate::bridge::PendingPromises,
3877) -> EventLoopStatus {
3878 match frame {
3879 SessionMessage::BridgeResponse(BridgeResponse {
3880 call_id,
3881 status,
3882 payload,
3883 reservation: _reservation,
3884 }) => {
3885 let (result, error) = if status == 1 {
3886 (None, Some(String::from_utf8_lossy(&payload).to_string()))
3887 } else if status == 2 || !payload.is_empty() {
3888 (Some(payload), None)
3890 } else {
3891 (None, None)
3892 };
3893 let _ = crate::bridge::resolve_pending_promise(
3894 scope, pending, call_id, status, result, error,
3895 );
3896 EventLoopStatus::Completed
3898 }
3899 SessionMessage::StreamEvent(StreamEvent {
3900 event_type,
3901 payload,
3902 }) => {
3903 let tc = &mut v8::TryCatch::new(scope);
3904 crate::stream::dispatch_stream_event(tc, &event_type, &payload);
3905 tc.perform_microtask_checkpoint();
3906 if let Some(exception) = tc.exception() {
3907 let (code, err) = execution::exception_to_result(tc, exception);
3908 return EventLoopStatus::Failed(code, err);
3909 }
3910 if let Some(err) = execution::take_unhandled_promise_rejection(tc) {
3911 return EventLoopStatus::Failed(1, err);
3912 }
3913 EventLoopStatus::Completed
3914 }
3915 SessionMessage::TerminateExecution => {
3916 scope.terminate_execution();
3917 EventLoopStatus::Terminated
3918 }
3919 _ => {
3920 EventLoopStatus::Completed
3922 }
3923 }
3924}
3925
3926#[cfg(test)]
3927mod tests {
3928 use super::*;
3929 use std::collections::HashSet;
3930
3931 const TEST_READY_BATCH_HANDLES: usize = 64;
3932
3933 fn test_manager(max: usize) -> SessionManager {
3935 test_manager_with_events(max).0
3936 }
3937
3938 fn test_manager_with_events(max: usize) -> (SessionManager, Receiver<RuntimeEventEnvelope>) {
3939 let (tx, _rx) = crossbeam_channel::unbounded();
3940 let router: CallIdRouter = Arc::new(BridgeCallRegistry::with_default_limit());
3941 let snap_cache = Arc::new(SnapshotCache::new(4));
3942 let runtime =
3943 agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default())
3944 .expect("create test process runtime")
3945 .context();
3946 let manager = SessionManager::new(max, tx, router, snap_cache, runtime);
3947 (manager, _rx)
3948 }
3949
3950 #[test]
3951 fn zero_cpu_time_limit_is_normalized_to_no_timeout() {
3952 assert_eq!(normalize_cpu_time_limit_ms(None), None);
3953 assert_eq!(normalize_cpu_time_limit_ms(Some(0)), None);
3954 assert_eq!(normalize_cpu_time_limit_ms(Some(1)), Some(1));
3955 }
3956
3957 #[test]
3958 fn vm_executor_permits_report_active_and_high_water_metrics() {
3959 let control: SlotControl = Arc::new((Mutex::new(0), Condvar::new()));
3960 let metrics = RuntimeMetrics::new();
3961
3962 let first = SessionSlotPermit::try_acquire(&control, 2, metrics.clone())
3963 .expect("acquire first VM executor");
3964 let second = SessionSlotPermit::try_acquire(&control, 2, metrics.clone())
3965 .expect("acquire second VM executor");
3966 let active = metrics.snapshot().executors[ExecutorMetricClass::Vm.index()].active;
3967 assert_eq!(active.current, 2);
3968 assert_eq!(active.high_water, 2);
3969
3970 drop(first);
3971 assert_eq!(
3972 metrics.snapshot().executors[ExecutorMetricClass::Vm.index()]
3973 .active
3974 .current,
3975 1
3976 );
3977
3978 drop(second);
3979 let released = metrics.snapshot().executors[ExecutorMetricClass::Vm.index()].active;
3980 assert_eq!(released.current, 0);
3981 assert_eq!(released.high_water, 2);
3982 }
3983
3984 #[test]
3985 fn configured_executor_and_command_bounds_drive_session_manager() {
3986 const SUBPROCESS_ENV: &str = "AGENTOS_V8_CONFIGURED_SESSION_MANAGER_SUBPROCESS";
3987 if std::env::var_os(SUBPROCESS_ENV).is_none() {
3988 let test_name =
3989 "session::tests::configured_executor_and_command_bounds_drive_session_manager";
3990 let output =
3991 std::process::Command::new(std::env::current_exe().expect("current test binary"))
3992 .arg(test_name)
3993 .arg("--exact")
3994 .arg("--nocapture")
3995 .env(SUBPROCESS_ENV, "1")
3996 .output()
3997 .unwrap_or_else(|error| panic!("spawn isolated test {test_name}: {error}"));
3998 assert!(
3999 output.status.success(),
4000 "isolated test {test_name} failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
4001 output.status.code(),
4002 String::from_utf8_lossy(&output.stdout),
4003 String::from_utf8_lossy(&output.stderr),
4004 );
4005 return;
4006 }
4007 let mut config = agentos_runtime::RuntimeConfig {
4008 max_active_vm_executors: 3,
4009 vm_executor_teardown_timeout_ms: 23,
4010 ..agentos_runtime::RuntimeConfig::default()
4011 };
4012 config.resources.max_handle_commands = 7;
4013 let runtime = agentos_runtime::SidecarRuntime::process(&config)
4014 .expect("configured process runtime")
4015 .context();
4016 let (event_tx, _event_rx) = crossbeam_channel::unbounded();
4017 let router: CallIdRouter = Arc::new(BridgeCallRegistry::with_default_limit());
4018 let mut manager = SessionManager::new(
4019 runtime.max_active_vm_executors(),
4020 event_tx,
4021 router,
4022 Arc::new(SnapshotCache::new(1)),
4023 runtime,
4024 );
4025
4026 assert_eq!(manager.max_concurrency, 3);
4027 assert_eq!(manager.executor_teardown_timeout, Duration::from_millis(23));
4028 manager
4029 .create_session("configured-bounds".into(), None, None, None)
4030 .expect("create bounded session");
4031 assert_eq!(manager.sessions["configured-bounds"].command_capacity, 7);
4032 manager
4033 .destroy_session("configured-bounds")
4034 .expect("destroy bounded session");
4035 }
4036
4037 fn expect_late_message_warning(
4038 rx: &Receiver<RuntimeEventEnvelope>,
4039 session_id: &str,
4040 error_code: &str,
4041 detail_fragment: &str,
4042 ) {
4043 let event = rx
4044 .recv_timeout(std::time::Duration::from_millis(200))
4045 .expect("late-message warning");
4046 match event.event {
4047 RuntimeEvent::Log {
4048 session_id: observed_session_id,
4049 channel,
4050 message,
4051 } => {
4052 assert_eq!(observed_session_id, session_id);
4053 assert_eq!(channel, 1, "late warnings should use stderr channel");
4054 assert!(
4055 message.contains(error_code),
4056 "warning should contain error code {error_code}, got {message}"
4057 );
4058 assert!(
4059 message.contains(detail_fragment),
4060 "warning should mention {detail_fragment}, got {message}"
4061 );
4062 }
4063 other => panic!("expected late-message warning log, got {other:?}"),
4064 }
4065 }
4066
4067 #[test]
4068 fn bridge_contract_function_partitions_cover_contract() {
4069 let contract = bridge_contract();
4070
4071 let expected_sync = contract
4072 .groups
4073 .iter()
4074 .filter(|group| {
4075 matches!(
4076 group.convention,
4077 BridgeCallConvention::Sync | BridgeCallConvention::SyncPromise
4078 )
4079 })
4080 .flat_map(|group| group.names.iter().map(String::as_str))
4081 .collect::<HashSet<_>>();
4082 let expected_async = contract
4083 .groups
4084 .iter()
4085 .filter(|group| group.convention == BridgeCallConvention::Async)
4086 .flat_map(|group| group.names.iter().map(String::as_str))
4087 .collect::<HashSet<_>>();
4088
4089 let sync_names = sync_bridge_fns();
4090 let async_names = async_bridge_fns();
4091 let registered_sync = sync_names.iter().copied().collect::<HashSet<_>>();
4092 let registered_async = async_names.iter().copied().collect::<HashSet<_>>();
4093
4094 assert_eq!(
4095 registered_sync, expected_sync,
4096 "sync bridge function partition drifted from crates/bridge/bridge-contract.json"
4097 );
4098 assert_eq!(
4099 registered_async, expected_async,
4100 "async bridge function partition drifted from crates/bridge/bridge-contract.json"
4101 );
4102 assert!(
4103 registered_sync.is_disjoint(®istered_async),
4104 "sync and async bridge function partitions must not overlap"
4105 );
4106 }
4107
4108 #[test]
4109 fn session_management() {
4110 {
4115 let mut mgr = test_manager(4);
4116
4117 mgr.create_session("session-aaa".into(), None, None, None)
4118 .expect("create session A");
4119 assert_eq!(mgr.session_count(), 1);
4120
4121 std::thread::sleep(std::time::Duration::from_millis(200));
4123
4124 mgr.destroy_session("session-aaa")
4126 .expect("destroy session A");
4127 assert_eq!(mgr.session_count(), 0);
4128 }
4129
4130 {
4132 let mut mgr = test_manager(4);
4133
4134 mgr.create_session("session-bbb".into(), None, None, None)
4135 .expect("create session B");
4136 mgr.create_session("session-ccc".into(), Some(16), None, None)
4137 .expect("create session C");
4138 assert_eq!(mgr.session_count(), 2);
4139
4140 std::thread::sleep(std::time::Duration::from_millis(200));
4141
4142 let err = mgr.create_session("session-bbb".into(), None, None, None);
4144 assert!(err.is_err());
4145 assert!(err.unwrap_err().contains("already exists"));
4146
4147 let err = mgr.send_to_session("missing", SessionMessage::TerminateExecution);
4149 assert!(err.is_err());
4150 assert!(err.unwrap_err().contains("does not exist"));
4151
4152 let err = mgr.destroy_session("no-such-session");
4154 assert!(err.is_err());
4155 assert!(err.unwrap_err().contains("does not exist"));
4156
4157 mgr.destroy_sessions(["session-bbb".into(), "session-ccc".into()]);
4158 assert_eq!(mgr.session_count(), 0);
4159 }
4160
4161 {
4163 let mut mgr = test_manager(2);
4164
4165 mgr.create_session("s1".into(), None, None, None)
4166 .expect("create s1");
4167 mgr.create_session("s2".into(), None, None, None)
4168 .expect("create s2");
4169 let error = mgr
4170 .create_session("s3".into(), None, None, None)
4171 .expect_err("third executor must be rejected before thread creation");
4172 assert!(error.contains("ERR_AGENTOS_VM_EXECUTOR_LIMIT"));
4173
4174 std::thread::sleep(std::time::Duration::from_millis(300));
4176
4177 assert_eq!(mgr.active_slot_count(), 2);
4179 assert_eq!(mgr.session_count(), 2);
4180
4181 mgr.destroy_session("s1").expect("destroy s1");
4183 mgr.create_session("s3".into(), None, None, None)
4184 .expect("create s3 after release");
4185 std::thread::sleep(std::time::Duration::from_millis(300));
4186 assert_eq!(mgr.active_slot_count(), 2);
4187 assert_eq!(mgr.session_count(), 2);
4188
4189 mgr.destroy_sessions(["s2".into(), "s3".into()]);
4191 std::thread::sleep(std::time::Duration::from_millis(100));
4192 assert_eq!(mgr.session_count(), 0);
4193 assert_eq!(mgr.active_slot_count(), 0);
4194 }
4195 }
4196
4197 #[test]
4198 fn detach_session_clears_call_id_routes_for_session() {
4199 let mut mgr = test_manager(1);
4200 mgr.create_session_with_output_generation(
4201 "session-route".into(),
4202 None,
4203 None,
4204 None,
4205 Some(7),
4206 None,
4207 )
4208 .expect("create session");
4209 let _waiter = mgr
4210 .call_id_router()
4211 .register_sync(&mgr.runtime, 0, 1, 42, "session-route", Some(7))
4212 .expect("register bridge call target");
4213
4214 assert!(
4215 mgr.detach_session_if_output_generation("session-route", 7)
4216 .expect("detach session"),
4217 "matching output generation should detach session"
4218 );
4219 assert!(
4220 mgr.call_id_router().pending_len() == 0,
4221 "detach should clear stale bridge call routes for the session"
4222 );
4223 assert_eq!(
4224 mgr.quarantined.len(),
4225 1,
4226 "detached executor join ownership must remain in the manager"
4227 );
4228 for handle in mgr.take_session_shutdown_handles() {
4229 handle.join().expect("join quarantined executor");
4230 }
4231 assert_eq!(mgr.active_slot_count(), 0);
4232 }
4233
4234 #[test]
4235 fn begin_destroy_session_removes_entry_before_finish() {
4236 let mut mgr = test_manager(1);
4237 mgr.create_session("two-phase".into(), None, None, None)
4238 .expect("create session");
4239
4240 let first_shutdown = mgr
4241 .begin_destroy_session("two-phase")
4242 .expect("begin destroy session");
4243 assert_eq!(
4244 mgr.session_count(),
4245 0,
4246 "entry should be removed before the shutdown is finished"
4247 );
4248
4249 let error = mgr
4252 .create_session("two-phase".into(), None, None, None)
4253 .expect_err("old generation must retain its executor permit");
4254 assert!(error.contains("ERR_AGENTOS_VM_EXECUTOR_LIMIT"));
4255 first_shutdown.finish();
4256
4257 mgr.create_session("two-phase".into(), None, None, None)
4258 .expect("re-create session after old generation joins");
4259 let second_shutdown = mgr
4260 .begin_destroy_session("two-phase")
4261 .expect("begin destroy re-created session");
4262 second_shutdown.finish();
4263 assert_eq!(mgr.session_count(), 0);
4264 }
4265
4266 #[test]
4267 fn shutdown_bypasses_a_full_ordinary_command_lane() {
4268 let mut mgr = test_manager(1);
4269 let command_capacity = mgr
4270 .runtime
4271 .resources()
4272 .usage(ResourceClass::HandleCommands)
4273 .limit
4274 .expect("configured command capacity");
4275 let (tx, rx) = crossbeam_channel::bounded(command_capacity);
4276 for index in 0..command_capacity {
4277 tx.send(SessionCommand::Message(SessionMessage::StreamEvent(
4278 StreamEvent {
4279 event_type: format!("ordinary-{index}"),
4280 payload: Vec::new(),
4281 },
4282 )))
4283 .expect("fill ordinary command lane");
4284 }
4285 let (shutdown_tx, shutdown_rx) = crossbeam_channel::bounded(1);
4286 let join_handle = thread::spawn(move || {
4287 shutdown_rx.recv().expect("dedicated shutdown token");
4288 drop(rx);
4289 });
4290 let (ready_broker, _ready_rx) =
4291 SessionReadiness::new(1, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4292 .expect("create session readiness");
4293 let session_resources = Arc::clone(mgr.runtime.resources());
4294 mgr.sessions.insert(
4295 String::from("full-command-lane"),
4296 SessionEntry {
4297 output_generation: None,
4298 tx,
4299 command_capacity,
4300 shutdown_tx,
4301 join_handle: Some(join_handle),
4302 isolate_handle: Arc::new(Mutex::new(None)),
4303 execution_abort: new_execution_abort(),
4304 execution_active: Arc::new(AtomicBool::new(false)),
4305 pause_control: Arc::new(SessionPauseControl::default()),
4306 ready_broker,
4307 session_resources,
4308 },
4309 );
4310
4311 mgr.begin_destroy_session("full-command-lane")
4312 .expect("begin destroy overloaded session")
4313 .finish();
4314 assert_eq!(mgr.session_count(), 0);
4315 }
4316
4317 #[test]
4318 fn execution_abort_is_durable_when_signaled_before_waiter_arm() {
4319 let execution_abort = new_execution_abort();
4320 signal_execution_abort_durable(&execution_abort, ExecutionAbortReason::Terminated);
4321
4322 let (_guard, receiver) = ActiveExecutionAbort::arm(&execution_abort);
4323 assert_eq!(
4324 receiver.recv_timeout(Duration::from_millis(10)),
4325 Err(crossbeam_channel::RecvTimeoutError::Disconnected),
4326 "a waiter armed after termination must observe it immediately"
4327 );
4328 }
4329
4330 #[test]
4331 fn session_shutdown_finish_clears_late_call_routes() {
4332 let mut mgr = test_manager(1);
4333 mgr.create_session("late-route".into(), None, None, None)
4334 .expect("create session");
4335
4336 let shutdown = mgr
4337 .begin_destroy_session("late-route")
4338 .expect("begin destroy session");
4339 let _waiter = mgr
4342 .call_id_router()
4343 .register_sync(&mgr.runtime, 0, 1, 42, "late-route", None)
4344 .expect("register late bridge call target");
4345
4346 shutdown.finish();
4347 assert!(
4348 mgr.call_id_router().pending_len() == 0,
4349 "finish should clear call routes registered during shutdown"
4350 );
4351 }
4352
4353 #[test]
4354 fn late_terminate_execution_is_logged_instead_of_silently_dropped() {
4355 let (mut mgr, rx) = test_manager_with_events(1);
4356 mgr.create_session("late-terminate".into(), None, None, None)
4357 .expect("create session");
4358
4359 mgr.send_to_session("late-terminate", SessionMessage::TerminateExecution)
4360 .expect("send late terminate");
4361
4362 expect_late_message_warning(
4363 &rx,
4364 "late-terminate",
4365 LATE_TERMINATE_EXECUTION_ERROR_CODE,
4366 "TerminateExecution",
4367 );
4368
4369 mgr.destroy_session("late-terminate")
4370 .expect("destroy session");
4371 }
4372
4373 #[test]
4374 fn late_stdin_end_is_an_expected_stale_control_event() {
4375 let (event_tx, event_rx) = crossbeam_channel::unbounded();
4376 handle_late_session_message(
4377 &RuntimeEventSender::from(event_tx),
4378 "completed-session",
4379 Some(7),
4380 SessionMessage::StreamEvent(StreamEvent {
4381 event_type: String::from("stdin_end"),
4382 payload: Vec::new(),
4383 }),
4384 );
4385
4386 assert!(
4387 event_rx.try_recv().is_err(),
4388 "an idempotent stdin EOF racing process exit must not become guest stderr"
4389 );
4390 }
4391
4392 #[test]
4393 fn late_bridge_response_is_logged_instead_of_silently_dropped() {
4394 let (mut mgr, rx) = test_manager_with_events(1);
4395 mgr.create_session("late-bridge".into(), None, None, None)
4396 .expect("create session");
4397
4398 mgr.send_to_session(
4399 "late-bridge",
4400 SessionMessage::BridgeResponse(BridgeResponse {
4401 call_id: 41,
4402 status: 0,
4403 payload: vec![0xAA, 0xBB],
4404 reservation: None,
4405 }),
4406 )
4407 .expect("send late bridge response");
4408
4409 expect_late_message_warning(
4410 &rx,
4411 "late-bridge",
4412 LATE_BRIDGE_RESPONSE_ERROR_CODE,
4413 "BridgeResponse",
4414 );
4415
4416 mgr.destroy_session("late-bridge").expect("destroy session");
4417 }
4418
4419 #[test]
4420 fn control_only_readiness_wake_becomes_a_command_and_rearms() {
4421 let mgr = test_manager(1);
4422 let (broker, wake_rx) = SessionReadiness::new(23, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4423 .expect("create session readiness");
4424
4425 broker.publish_timer(91).expect("publish first timer");
4426 let first_wake = wake_rx.try_recv().expect("first timer wake");
4427 let first_batch = match ready_batch_command(&broker, first_wake).expect("timer command") {
4428 SessionCommand::ReadyBatch(batch) => batch,
4429 _ => panic!("timer wake must produce a readiness command"),
4430 };
4431 assert!(first_batch.entries.is_empty());
4432 assert!(first_batch.timers_ready);
4433 assert_eq!(
4434 broker
4435 .drain_timers(&first_batch)
4436 .expect("drain first timer"),
4437 vec![91]
4438 );
4439 broker
4440 .complete_batch(&first_batch, &[])
4441 .expect("complete first timer wake");
4442
4443 broker.publish_timer(92).expect("publish second timer");
4444 let second_wake = wake_rx
4445 .try_recv()
4446 .expect("completing a control-only batch must rearm the wake lane");
4447 let second_batch = match ready_batch_command(&broker, second_wake).expect("second command")
4448 {
4449 SessionCommand::ReadyBatch(batch) => batch,
4450 _ => panic!("second timer wake must produce a readiness command"),
4451 };
4452 assert!(second_batch.entries.is_empty());
4453 assert!(second_batch.timers_ready);
4454 }
4455
4456 #[test]
4457 fn admitted_session_command_precedes_later_signal_wake() {
4458 let mgr = test_manager(1);
4459 let (broker, ready_rx) = SessionReadiness::new(31, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4460 .expect("create session readiness");
4461 let (tx, rx) = crossbeam_channel::bounded(2);
4462 let (_shutdown_tx, shutdown_rx) = crossbeam_channel::bounded(1);
4463
4464 tx.send(SessionCommand::Message(SessionMessage::StreamEvent(
4465 StreamEvent {
4466 event_type: String::from("execute-admitted-first"),
4467 payload: Vec::new(),
4468 },
4469 )))
4470 .expect("queue ordinary session command");
4471 broker.publish_signal(15).expect("publish later SIGTERM");
4472
4473 assert!(matches!(
4474 recv_session_command(&rx, &shutdown_rx, &ready_rx, &broker),
4475 Some(SessionCommand::Message(SessionMessage::StreamEvent(_)))
4476 ));
4477 let Some(SessionCommand::ReadyBatch(batch)) =
4478 recv_session_command(&rx, &shutdown_rx, &ready_rx, &broker)
4479 else {
4480 panic!("later signal wake must remain queued after the admitted command");
4481 };
4482 assert!(batch.signals_ready);
4483 assert_eq!(
4484 broker.drain_signals(&batch).expect("drain signal"),
4485 vec![15]
4486 );
4487 broker
4488 .complete_batch(&batch, &[])
4489 .expect("complete signal wake");
4490 }
4491
4492 #[test]
4493 fn readiness_flood_uses_one_wake_and_carries_only_capability_identity() {
4494 let mgr = test_manager(1);
4495 let (broker, wake_rx) = SessionReadiness::new(7, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4496 .expect("create session readiness");
4497 for _ in 0..1_000_000 {
4498 broker
4499 .publish(41, 3, ReadyFlags::READABLE)
4500 .expect("publish readiness");
4501 }
4502
4503 let wake = wake_rx.try_recv().expect("one coalesced wake");
4504 assert!(
4505 wake_rx.try_recv().is_err(),
4506 "wake lane must have capacity one"
4507 );
4508 let batch = broker.take_batch(wake).expect("take readiness batch");
4509 assert_eq!(batch.entries.len(), 1);
4510 assert_eq!(batch.entries[0].capability_id, 41);
4511 assert_eq!(batch.entries[0].capability_generation, 3);
4512 assert_eq!(batch.entries[0].flags, ReadyFlags::READABLE);
4513 assert_eq!(batch.entries[0].revision, 1_000_000);
4514 broker
4515 .complete_batch(&batch, &batch.entries)
4516 .expect("complete readiness");
4517 assert_eq!(
4518 broker.broker.pending_handle_count().expect("pending count"),
4519 0
4520 );
4521 }
4522
4523 #[test]
4524 fn readiness_batch_honors_vm_reactor_work_quantum_override() {
4525 let mgr = test_manager(1);
4526 let (broker, wake_rx) =
4527 SessionReadiness::new(29, &mgr.runtime, 2).expect("create bounded readiness");
4528 for capability_id in 1..=3 {
4529 broker
4530 .publish(capability_id, 1, ReadyFlags::READABLE)
4531 .expect("publish readiness");
4532 }
4533
4534 let first_wake = wake_rx.recv().expect("first coalesced wake");
4535 let first_batch = broker.take_batch(first_wake).expect("first bounded batch");
4536 assert_eq!(
4537 first_batch.entries.len(),
4538 2,
4539 "limits.reactor.workQuantum must cap one V8 readiness turn"
4540 );
4541 broker
4542 .complete_batch(&first_batch, &first_batch.entries)
4543 .expect("complete first bounded batch");
4544
4545 let second_wake = wake_rx.recv().expect("replacement wake for remaining work");
4546 let second_batch = broker
4547 .take_batch(second_wake)
4548 .expect("second bounded batch");
4549 assert_eq!(second_batch.entries.len(), 1);
4550 }
4551
4552 #[test]
4553 fn readiness_batch_rejects_zero_vm_reactor_work_quantum() {
4554 let mgr = test_manager(1);
4555 let error = SessionReadiness::new(30, &mgr.runtime, 0)
4556 .expect_err("zero work quantum must fail closed");
4557 assert!(error.contains("limits.reactor.workQuantum"));
4558 }
4559
4560 #[test]
4561 fn readiness_rejects_a_stale_capability_generation_without_replacing_state() {
4562 let mgr = test_manager(1);
4563 let (broker, wake_rx) = SessionReadiness::new(13, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4564 .expect("create session readiness");
4565 broker
4566 .publish(9, 4, ReadyFlags::READABLE)
4567 .expect("publish live capability");
4568 let error = broker
4569 .publish(9, 3, ReadyFlags::CLOSE)
4570 .expect_err("stale capability generation must fail");
4571 assert!(error.contains("ERR_AGENTOS_READY_STALE_CAPABILITY"));
4572 assert!(wake_rx.len() <= 1, "wake lane must stay capacity one");
4573 let wake = wake_rx.recv().expect("durable wake");
4574 let batch = broker.take_batch(wake).expect("take readiness batch");
4575 assert_eq!(batch.entries.len(), 1);
4576 assert_eq!(batch.entries[0].capability_generation, 4);
4577 assert_eq!(batch.entries[0].flags, ReadyFlags::READABLE);
4578 }
4579
4580 #[test]
4581 fn readiness_before_guest_registration_remains_pending_until_dispatch_succeeds() {
4582 let mgr = test_manager(1);
4583 let (broker, wake_rx) = SessionReadiness::new(17, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4584 .expect("create session readiness");
4585 broker
4586 .publish(33, 8, ReadyFlags::READABLE)
4587 .expect("publish readiness before guest registration");
4588
4589 let first_wake = wake_rx.recv().expect("initial wake");
4590 let first_batch = broker.take_batch(first_wake).expect("initial batch");
4591 broker
4592 .complete_batch(&first_batch, &[])
4593 .expect("preserve readiness when guest target is absent");
4594 assert_eq!(
4595 broker.broker.pending_handle_count().expect("pending count"),
4596 1
4597 );
4598
4599 let retry_wake = wake_rx.recv().expect("retry wake after registration");
4600 let retry_batch = broker.take_batch(retry_wake).expect("retry batch");
4601 broker
4602 .complete_batch(&retry_batch, &retry_batch.entries)
4603 .expect("acknowledge readiness after target runs");
4604 assert_eq!(
4605 broker.broker.pending_handle_count().expect("pending count"),
4606 0
4607 );
4608 }
4609
4610 #[test]
4611 fn readiness_dispatch_failure_completes_wake_before_session_reuse() {
4612 let mgr = test_manager(1);
4613 let (broker, wake_rx) = SessionReadiness::new(18, &mgr.runtime, TEST_READY_BATCH_HANDLES)
4614 .expect("create reusable session readiness");
4615 broker
4616 .publish(34, 9, ReadyFlags::READABLE)
4617 .expect("publish readiness before failing dispatch");
4618
4619 let first_wake = wake_rx.recv().expect("initial wake");
4620 let first_batch = broker.take_batch(first_wake).expect("initial batch");
4621 let status = complete_ready_batch_dispatch(
4622 &broker,
4623 &first_batch,
4624 &[],
4625 EventLoopStatus::Failed(
4626 1,
4627 ExecutionError {
4628 error_type: String::from("Error"),
4629 message: String::from("injected readiness handler failure"),
4630 stack: String::new(),
4631 code: Some(String::from("ERR_TEST_READY_DISPATCH")),
4632 },
4633 ),
4634 );
4635 assert!(matches!(status, EventLoopStatus::Failed(1, _)));
4636
4637 let retry_wake = wake_rx
4638 .try_recv()
4639 .expect("failed dispatch must complete and rearm the wake for session reuse");
4640 let retry_batch = broker.take_batch(retry_wake).expect("reused-session batch");
4641 assert_eq!(retry_batch.entries, first_batch.entries);
4642 broker
4643 .complete_batch(&retry_batch, &retry_batch.entries)
4644 .expect("reused session can acknowledge the retried readiness");
4645 }
4646
4647 #[test]
4660 fn reset_pending_promises_drops_resolver_globals_before_isolate_teardown() {
4661 use crate::bridge::{register_async_bridge_fns, PendingPromises};
4662 use crate::host_call::BridgeCallContext;
4663 use crate::isolate;
4664 use std::process::Command;
4665
4666 const SUBPROCESS_ENV: &str = "AGENTOS_V8_RESET_PENDING_PROMISES_SUBPROCESS";
4671 if std::env::var_os(SUBPROCESS_ENV).is_none() {
4672 let output = Command::new(std::env::current_exe().expect("current test binary"))
4673 .arg("session::tests::reset_pending_promises_drops_resolver_globals_before_isolate_teardown")
4674 .arg("--exact")
4675 .arg("--nocapture")
4676 .env(SUBPROCESS_ENV, "1")
4677 .output()
4678 .expect("spawn reset-pending-promises subprocess");
4679 assert!(
4680 output.status.success(),
4681 "reset-pending-promises subprocess failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
4682 output.status.code(),
4683 String::from_utf8_lossy(&output.stdout),
4684 String::from_utf8_lossy(&output.stderr)
4685 );
4686 return;
4687 }
4688
4689 isolate::init_v8_platform();
4690
4691 let mut v8_isolate = isolate::create_isolate(None);
4692 let context = isolate::create_context(&mut v8_isolate);
4693 let scope = &mut v8::HandleScope::new(&mut v8_isolate);
4694 let context = v8::Local::new(scope, &context);
4695 let scope = &mut v8::ContextScope::new(scope, context);
4696
4697 let bridge_ctx = BridgeCallContext::new(
4698 Box::new(std::io::sink()),
4699 Box::new(std::io::empty()),
4700 String::from("reset-pending-test"),
4701 );
4702 let mut pending = PendingPromises::new();
4703
4704 const REGISTERED: usize = 8;
4709 let _async_fns = register_async_bridge_fns(
4710 scope,
4711 &bridge_ctx as *const BridgeCallContext,
4712 &pending as *const PendingPromises,
4713 &["_asyncFn"],
4714 );
4715 let source = format!("for (let i = 0; i < {REGISTERED}; i++) {{ _asyncFn(i); }}");
4716 {
4717 let tc = &mut v8::TryCatch::new(scope);
4718 let code = v8::String::new(tc, &source).unwrap();
4719 let script = v8::Script::compile(tc, code, None).unwrap();
4720 assert!(
4721 script.run(tc).is_some(),
4722 "async bridge calls should register resolvers, not throw"
4723 );
4724 assert!(!tc.has_caught(), "async bridge calls should not throw");
4725 }
4726 assert_eq!(
4727 pending.len(),
4728 REGISTERED,
4729 "each _asyncFn call must register a pending resolver Global"
4730 );
4731
4732 reset_pending_promises(&mut pending);
4736
4737 assert_eq!(
4738 pending.len(),
4739 0,
4740 "reset_pending_promises must drop all pending resolver Globals before isolate teardown"
4741 );
4742
4743 }
4746}