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