1use std::time::Duration;
4
5use serde::Serialize;
6use serde_json::{json, Map, Value};
7
8use crate::clock::Clock;
9use crate::compact::{compact_form_field, compact_message, compact_permission, Message, ToolsMode};
10use crate::engine::Engine;
11use crate::error::{ErrorCode, ToolError, ToolResult};
12use crate::settings::Settings;
13use crate::transport::{Transport, WaitOutcome};
14
15use super::SessionWaitArgs;
16
17const PAGE_LIMIT: usize = 200;
19const PENDING_CEILING: usize = 20;
21const CHILD_SLICE_MS: i64 = 30_000;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum WaitState {
27 Idle,
28 Failed,
29 Interrupted,
30 Running,
31 AwaitingPermission,
32 AwaitingInput,
33 Stalled,
34 BudgetExceeded,
35 Timeout,
36}
37
38impl WaitState {
39 fn name(self) -> &'static str {
40 match self {
41 Self::Idle => "idle",
42 Self::Failed => "failed",
43 Self::Interrupted => "interrupted",
44 Self::Running => "running",
45 Self::AwaitingPermission => "awaiting_permission",
46 Self::AwaitingInput => "awaiting_input",
47 Self::Stalled => "stalled",
48 Self::BudgetExceeded => "budget_exceeded",
49 Self::Timeout => "timeout",
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum AnchorSource {
56 Inbox { parked_since: i64 },
58 Message,
60 Neither { newest_ms: i64 },
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Anchor {
66 pub id: String,
67 pub created_ms: i64,
68 pub source: AnchorSource,
69}
70
71pub struct Snapshot {
73 pub messages: Vec<Message>,
74 pub inbox: Vec<Value>,
75 pub active: bool,
76 pub permissions: Vec<Value>,
77 pub forms: Vec<Value>,
78 pub now_ms: i64,
79}
80
81#[derive(Debug, Clone, PartialEq)]
82pub struct Evaluation {
83 pub state: WaitState,
84 pub outcome: Option<String>,
86 pub error: Option<Value>,
88 pub concurrent_turn: Option<Option<String>>,
90 pub turn_s: Option<i64>,
91}
92
93#[derive(Debug, Default)]
94pub struct PageState {
95 pub newest_cursor_previous: Option<String>,
96}
97
98#[derive(Debug, Serialize)]
99pub struct WaitResult {
100 pub state: WaitState,
101 pub elapsed_s: i64,
102 pub turn_s: Option<i64>,
103 pub inbox_pending: bool,
104 pub tokens: Option<Value>,
105 pub last_assistant_text: Option<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub pending_permissions: Option<Vec<Value>>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub pending_forms: Option<Vec<Value>>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub more_permissions: Option<usize>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub more_forms: Option<usize>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub outcome: Option<String>,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub verify_hint: Option<Value>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub concurrent_turn: Option<Option<String>>,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub error: Option<Value>,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub parked_s: Option<i64>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub pair: Option<Value>,
126}
127
128fn is_user(m: &Message) -> bool {
129 m.r#type == "user"
130}
131
132fn is_idle(m: &Message) -> bool {
133 m.r#type == "idle"
134}
135
136fn item_id(v: &Value) -> Option<&str> {
137 v.get("id").and_then(Value::as_str)
138}
139
140fn item_created(v: &Value) -> Option<i64> {
141 v.pointer("/time/created").and_then(Value::as_i64)
142}
143
144fn parked(anchor: &Anchor, snap: &Snapshot) -> bool {
145 matches!(anchor.source, AnchorSource::Inbox { .. })
146 && snap
147 .inbox
148 .iter()
149 .any(|i| item_id(i) == Some(anchor.id.as_str()))
150}
151
152fn turn_start(anchor: &Anchor, snap: &Snapshot, end: i64) -> Option<i64> {
155 let oldest_open_user = snap
156 .messages
157 .iter()
158 .filter(|u| is_user(u) && u.created <= end)
159 .filter(|u| {
160 !snap
161 .messages
162 .iter()
163 .any(|m| is_idle(m) && m.created > u.created && m.created < end)
164 })
165 .map(|u| u.created)
166 .min();
167 let has_user = snap.messages.iter().any(is_user);
168 if !has_user && !matches!(anchor.source, AnchorSource::Inbox { .. }) {
169 return None;
170 }
171 Some(oldest_open_user.map_or(anchor.created_ms, |u| u.min(anchor.created_ms)))
172}
173
174fn seconds(ms: i64) -> i64 {
175 (ms / 1000).max(0)
176}
177
178pub fn evaluate(anchor: &Anchor, snap: &Snapshot, settings: &Settings) -> Evaluation {
180 let parked = parked(anchor, snap);
181 let turn = |end: i64| turn_start(anchor, snap, end).map(|s| seconds(end - s));
182
183 if !parked {
184 let marker = snap
185 .messages
186 .iter()
187 .filter(|m| is_idle(m) && m.created > anchor.created_ms)
188 .min_by_key(|m| m.created);
189 if let Some(marker) = marker {
190 let state = match marker.outcome.as_deref() {
191 Some("failed") => WaitState::Failed,
192 Some("interrupted") => WaitState::Interrupted,
193 _ => WaitState::Idle,
194 };
195 let concurrent = snap
196 .messages
197 .iter()
198 .rev()
199 .find(|m| is_user(m) && m.created > marker.created)
200 .map(|m| m.id.clone());
201 return Evaluation {
202 state,
203 outcome: Some(state.name().to_string()),
204 error: (state == WaitState::Failed)
206 .then(|| marker.tools.first().cloned())
207 .flatten(),
208 concurrent_turn: Some(concurrent),
209 turn_s: turn(marker.created),
210 };
211 }
212 }
213
214 let turn_s = turn(snap.now_ms);
215 let verdict = |state| Evaluation {
216 state,
217 outcome: None,
218 error: None,
219 concurrent_turn: None,
220 turn_s,
221 };
222 if !snap.permissions.is_empty() {
223 return verdict(WaitState::AwaitingPermission);
224 }
225 if !snap.forms.is_empty() {
226 return verdict(WaitState::AwaitingInput);
227 }
228 if turn_s.is_some_and(|t| t > settings.max_turn_s as i64) {
229 return verdict(WaitState::BudgetExceeded);
230 }
231 if !parked && !snap.active {
232 let newest = snap.messages.iter().max_by_key(|m| m.created);
233 let newest_is_marker_after_anchor =
234 newest.is_some_and(|m| is_idle(m) && m.created > anchor.created_ms);
235 let clock = match (newest, &anchor.source) {
237 (Some(m), _) => Some(m.created),
238 (None, AnchorSource::Neither { .. }) => Some(anchor.created_ms),
239 (None, _) => None,
240 };
241 if let Some(clock) = clock {
242 if !newest_is_marker_after_anchor
243 && snap.now_ms - clock >= settings.stall_s as i64 * 1000
244 {
245 return verdict(WaitState::Stalled);
246 }
247 }
248 }
249 verdict(WaitState::Running)
250}
251
252fn last_assistant_text(anchor: &Anchor, messages: &[Message]) -> Option<String> {
254 messages
255 .iter()
256 .rev()
257 .filter(|m| m.r#type == "assistant" && m.created > anchor.created_ms)
258 .find_map(|m| m.last_text.clone())
259}
260
261fn message_from_raw(raw: &Value) -> Message {
262 let mut m = compact_message(raw, false, ToolsMode::None);
263 if m.r#type == "idle" {
264 if let Some(e) = raw.get("error") {
265 let name = e.get("name").or_else(|| e.get("type")).cloned();
266 m.tools.push(json!({
267 "name": name.unwrap_or(Value::Null),
268 "message": e.get("message").cloned().unwrap_or(Value::Null),
269 }));
270 }
271 }
272 m
273}
274
275fn push_new(out: &mut Vec<Message>, page: Vec<Message>) {
276 for m in page {
277 if !out.iter().any(|k| k.id == m.id) {
278 out.push(m);
279 }
280 }
281}
282
283fn rows(v: &Value) -> Vec<Value> {
284 v.as_array().cloned().unwrap_or_default()
285}
286
287fn cursor_str(cursor: &Option<Value>, key: &str) -> Option<String> {
288 cursor
289 .as_ref()
290 .and_then(|c| c.get(key))
291 .and_then(Value::as_str)
292 .map(str::to_string)
293}
294
295fn compact_form(raw: &Value) -> Value {
296 let mut o = Map::new();
297 for k in ["id", "title", "metadata"] {
298 if let Some(v) = raw.get(k) {
299 o.insert(k.to_string(), v.clone());
300 }
301 }
302 if let Some(fields) = raw.get("fields").and_then(Value::as_array) {
303 o.insert(
304 "fields".into(),
305 Value::Array(fields.iter().map(compact_form_field).collect()),
306 );
307 }
308 Value::Object(o)
309}
310
311fn ceiling(list: &[Value], compact: fn(&Value) -> Value) -> (Option<Vec<Value>>, Option<usize>) {
313 if list.is_empty() {
314 return (None, None);
315 }
316 let kept: Vec<Value> = list.iter().take(PENDING_CEILING).map(compact).collect();
317 let more = list.len().saturating_sub(PENDING_CEILING);
318 (Some(kept), (more > 0).then_some(more))
319}
320
321enum Stop<'a> {
322 Find(&'a str),
324 Anchor(&'a Anchor),
326}
327
328impl Stop<'_> {
329 fn ts(&self, m: &Message) -> i64 {
331 match self {
332 Stop::Find(_) => m.created,
333 Stop::Anchor(a) => a.created_ms,
334 }
335 }
336
337 fn hit(&self, m: &Message) -> bool {
338 match self {
339 Stop::Find(id) => m.id == *id,
340 Stop::Anchor(a) => m.id == a.id || m.created <= a.created_ms,
341 }
342 }
343}
344
345struct Located {
347 anchor: Anchor,
348 messages: Vec<Message>,
349 inbox: Vec<Value>,
350 cut: bool,
352}
353
354struct Lists {
356 permissions: Vec<Value>,
357 forms: Vec<Value>,
358}
359
360enum PollEnd {
361 Found(Lists),
363 Deadline(Lists),
365 Failed(ToolError),
367}
368
369struct ChildState {
371 messages: Vec<Message>,
372 pages: PageState,
373}
374
375struct ChildDone {
377 inbox: Vec<Value>,
378 active: bool,
379}
380
381enum SliceEnd {
382 Child(ChildDone),
383 Poll(Lists),
384}
385
386impl<T: Transport, C: Clock> Engine<T, C> {
387 async fn list_op(
388 &self,
389 op: &str,
390 session: &str,
391 ) -> Result<(Value, Option<Value>, Option<Value>), ToolError> {
392 self.call_json(
393 op,
394 vec![("sessionID".to_string(), session.to_string())],
395 None,
396 )
397 .await
398 }
399
400 async fn read_pages(
404 &self,
405 session: &str,
406 stop: Stop<'_>,
407 state: &mut PageState,
408 cut_at: i64,
409 ) -> Result<(Vec<Message>, bool, bool), ToolError> {
410 let mut all: Vec<Message> = Vec::new();
411 let mut cursor: Option<String> = None;
412 let mut first = true;
413 let mut hit_ts: Option<i64> = None;
414 let mut cut = false;
415 let found = loop {
416 let mut params = vec![
417 ("sessionID".to_string(), session.to_string()),
418 ("limit".to_string(), PAGE_LIMIT.to_string()),
419 ];
420 match &cursor {
421 Some(c) => params.push(("cursor".into(), c.clone())),
423 None => params.push(("order".into(), "desc".into())),
424 }
425 let (data, cur, _) = self.call_json("session.message.list", params, None).await?;
426 if first {
427 state.newest_cursor_previous = cursor_str(&cur, "previous");
428 first = false;
429 }
430 let page: Vec<Message> = rows(&data).iter().map(message_from_raw).collect();
431 let empty = page.is_empty();
432 if hit_ts.is_none() {
433 hit_ts = page.iter().find(|m| stop.hit(m)).map(|m| stop.ts(m));
434 }
435 all.extend(page);
436 let done = hit_ts.is_some_and(|ts| all.iter().any(|m| is_idle(m) && m.created < ts));
439 match cursor_str(&cur, "next") {
440 Some(_) if !done && !empty && self.clock().now_ms() >= cut_at => {
443 cut = true;
444 break hit_ts.is_some();
445 }
446 Some(next) if !done && !empty => cursor = Some(next),
447 _ => break hit_ts.is_some(),
448 }
449 };
450 all.reverse();
451 Ok((all, found, cut))
452 }
453
454 pub async fn read_messages_since(
457 &self,
458 session: &str,
459 anchor: &Anchor,
460 state: &mut PageState,
461 ) -> Result<Vec<Message>, ToolError> {
462 let mut out = Vec::new();
463 self.read_incremental(session, anchor, state, &mut out)
464 .await?;
465 Ok(out)
466 }
467
468 async fn read_incremental(
471 &self,
472 session: &str,
473 anchor: &Anchor,
474 state: &mut PageState,
475 out: &mut Vec<Message>,
476 ) -> Result<(), ToolError> {
477 let Some(mut cursor) = state.newest_cursor_previous.clone() else {
478 let (page, _, _) = self
479 .read_pages(session, Stop::Anchor(anchor), state, i64::MAX)
480 .await?;
481 push_new(out, page);
482 return Ok(());
483 };
484 loop {
485 let params = vec![
486 ("sessionID".to_string(), session.to_string()),
487 ("limit".to_string(), PAGE_LIMIT.to_string()),
488 ("cursor".to_string(), cursor.clone()),
489 ];
490 let (data, cur, _) = self.call_json("session.message.list", params, None).await?;
491 let mut page: Vec<Message> = rows(&data).iter().map(message_from_raw).collect();
492 let full = page.len() >= PAGE_LIMIT;
493 if page.is_empty() {
494 break;
495 }
496 page.reverse();
497 push_new(out, page);
498 match cursor_str(&cur, "previous") {
499 Some(prev) => {
500 state.newest_cursor_previous = Some(prev.clone());
501 if !full {
502 break;
503 }
504 cursor = prev;
505 }
506 None => break,
507 }
508 }
509 Ok(())
510 }
511
512 async fn locate(
515 &self,
516 session: &str,
517 after: Option<&str>,
518 call_start: i64,
519 cut_at: i64,
520 state: &mut PageState,
521 ) -> Result<Option<Located>, ToolError> {
522 let Some(after) = after else {
523 let params = vec![
524 ("sessionID".to_string(), session.to_string()),
525 ("order".to_string(), "desc".to_string()),
526 ("type".to_string(), "user".to_string()),
527 ("limit".to_string(), "1".to_string()),
528 ];
529 let (data, _, _) = self.call_json("session.message.list", params, None).await?;
530 let Some(newest_user) = rows(&data).first().map(message_from_raw) else {
531 return Ok(None);
532 };
533 let anchor = Anchor {
534 id: newest_user.id,
535 created_ms: newest_user.created,
536 source: AnchorSource::Message,
537 };
538 let (messages, _, cut) = self
539 .read_pages(session, Stop::Anchor(&anchor), state, cut_at)
540 .await?;
541 return Ok(Some(Located {
543 anchor,
544 messages,
545 inbox: Vec::new(),
546 cut,
547 }));
548 };
549
550 let (inbox, _, _) = self.list_op("session.inbox.list", session).await?;
551 let inbox = rows(&inbox);
552 if let Some(created) = inbox
553 .iter()
554 .find(|i| item_id(i) == Some(after))
555 .and_then(item_created)
556 {
557 let anchor = Anchor {
558 id: after.to_string(),
559 created_ms: created,
560 source: AnchorSource::Inbox {
561 parked_since: created,
562 },
563 };
564 let (messages, _, cut) = self
565 .read_pages(session, Stop::Anchor(&anchor), state, cut_at)
566 .await?;
567 return Ok(Some(Located {
568 anchor,
569 messages,
570 inbox,
571 cut,
572 }));
573 }
574
575 let (messages, found, cut) = self
577 .read_pages(session, Stop::Find(after), state, cut_at)
578 .await?;
579 let anchor = match messages.iter().find(|m| m.id == after) {
580 Some(m) if found => Anchor {
581 id: after.to_string(),
582 created_ms: m.created,
583 source: AnchorSource::Message,
584 },
585 _ => {
586 let newest_ms = messages
587 .iter()
588 .map(|m| m.created)
589 .max()
590 .unwrap_or(call_start);
593 Anchor {
594 id: after.to_string(),
595 created_ms: newest_ms,
596 source: AnchorSource::Neither { newest_ms },
597 }
598 }
599 };
600 Ok(Some(Located {
601 anchor,
602 messages,
603 inbox,
604 cut,
605 }))
606 }
607
608 pub async fn resolve_anchor(
612 &self,
613 session: &str,
614 after: Option<&str>,
615 ) -> Result<Anchor, ToolError> {
616 let mut state = PageState::default();
617 self.locate(session, after, self.clock().now_ms(), i64::MAX, &mut state)
618 .await?
619 .map(|l| l.anchor)
620 .ok_or_else(|| {
621 ToolError::new(
622 ErrorCode::InvalidRequest,
623 "the session has no user message to anchor on",
624 )
625 })
626 }
627
628 async fn is_active(&self, session: &str) -> Result<bool, ToolError> {
631 let (data, _, _) = self.call_json("session.active", Vec::new(), None).await?;
632 Ok(data.get(session).is_some())
633 }
634
635 async fn tokens_of(&self, session: &str) -> Option<Value> {
636 let (s, _, _) = self.list_op("session.get", session).await.ok()?;
637 Some(s.get("tokens").cloned().unwrap_or(Value::Null))
638 }
639
640 async fn poll_once(&self, session: &str) -> Result<Lists, ToolError> {
642 let (permissions, _, _) = self.list_op("session.permission.list", session).await?;
643 let (forms, _, _) = self.list_op("session.form.list", session).await?;
644 Ok(Lists {
645 permissions: rows(&permissions),
646 forms: rows(&forms),
647 })
648 }
649
650 async fn poll_loop(
653 &self,
654 session: &str,
655 started: i64,
656 timeout_ms: i64,
657 poll_ms: i64,
658 ) -> PollEnd {
659 let mut failed = 0u8;
660 loop {
661 let at_deadline = self.clock().now_ms() - started >= timeout_ms;
662 match self.poll_once(session).await {
663 Ok(lists) => {
664 failed = 0;
665 if !lists.permissions.is_empty() || !lists.forms.is_empty() {
666 return PollEnd::Found(lists);
667 }
668 if at_deadline {
669 return PollEnd::Deadline(lists);
670 }
671 }
672 Err(e) => {
673 failed += 1;
674 if failed >= 2 {
675 return PollEnd::Failed(e);
676 }
677 if at_deadline {
678 return PollEnd::Deadline(Lists {
679 permissions: Vec::new(),
680 forms: Vec::new(),
681 });
682 }
683 }
684 }
685 let elapsed = self.clock().now_ms() - started;
686 let next = (elapsed / poll_ms + 1) * poll_ms;
687 let target = next.min(timeout_ms);
688 self.clock()
689 .sleep(Duration::from_millis((target - elapsed).max(0) as u64))
690 .await;
691 }
692 }
693
694 async fn child_loop(
697 &self,
698 session: &str,
699 anchor: &Anchor,
700 deadline_ms: i64,
701 poll_ms: i64,
702 cs: &mut ChildState,
703 ) -> Result<ChildDone, ToolError> {
704 loop {
705 let remaining = deadline_ms - self.clock().now_ms();
706 if remaining <= 0 {
707 return std::future::pending().await;
709 }
710 let outcome = self
711 .wait_idle(
712 session,
713 Duration::from_millis(remaining.min(CHILD_SLICE_MS) as u64),
714 )
715 .await?;
716 let (inbox, _, _) = self.list_op("session.inbox.list", session).await?;
717 let active = self.is_active(session).await?;
718 self.read_incremental(session, anchor, &mut cs.pages, &mut cs.messages)
719 .await?;
720 let snap = Snapshot {
721 messages: cs.messages.clone(),
722 inbox: rows(&inbox),
723 active,
724 permissions: Vec::new(),
726 forms: Vec::new(),
727 now_ms: self.clock().now_ms(),
728 };
729 if evaluate(anchor, &snap, &self.settings).state != WaitState::Running {
730 return Ok(ChildDone {
731 inbox: snap.inbox,
732 active: snap.active,
733 });
734 }
735 if outcome == WaitOutcome::Idle {
736 self.clock()
738 .sleep(Duration::from_millis(poll_ms as u64))
739 .await;
740 }
741 }
742 }
743
744 fn pair_of(&self) -> Option<Value> {
745 self.env.session.as_ref().map(|_| {
747 let (tui, claude) = self.pair_liveness();
748 json!({"tui": tui.as_str(), "claude": claude.as_str()})
749 })
750 }
751
752 pub async fn session_wait(&self, a: SessionWaitArgs) -> ToolResult {
753 let session = self.resolve_session(a.session_id.as_deref())?;
754 self.guard_created(&session)?;
755 for (name, v) in [("timeout_s", a.timeout_s), ("poll_s", a.poll_s)] {
756 if v == Some(0) {
757 return Err(ToolError::new(
758 ErrorCode::InvalidArgument,
759 format!("{name} must be at least 1"),
760 ));
761 }
762 }
763 let timeout_s = a
764 .timeout_s
765 .unwrap_or(self.settings.wait_timeout_s)
766 .min(self.settings.wait_max_s);
767 let poll_s = a.poll_s.unwrap_or(self.settings.poll_s).max(1);
768 let timeout_ms = timeout_s as i64 * 1000;
769 let poll_ms = poll_s as i64 * 1000;
770 let started = self.clock().now_ms();
771 let deadline_ms = started + timeout_ms;
772 let pair = self.pair_of();
773
774 let mut cs = ChildState {
775 messages: Vec::new(),
776 pages: PageState::default(),
777 };
778 let Some(located) = self
779 .locate(
780 &session,
781 a.after.as_deref(),
782 started,
783 deadline_ms,
784 &mut cs.pages,
785 )
786 .await?
787 else {
788 let tokens = self.tokens_of(&session).await;
789 let result = WaitResult {
790 state: WaitState::Idle,
791 elapsed_s: seconds(self.clock().now_ms() - started),
792 turn_s: None,
793 inbox_pending: false,
794 tokens: tokens.filter(|t| !t.is_null()),
795 last_assistant_text: None,
796 pending_permissions: None,
797 pending_forms: None,
798 more_permissions: None,
799 more_forms: None,
800 outcome: Some("idle".into()),
801 verify_hint: None,
802 concurrent_turn: Some(None),
803 error: None,
804 parked_s: None,
805 pair,
806 };
807 return Ok(serde_json::to_value(result).unwrap_or(Value::Null));
808 };
809 let Located {
810 anchor,
811 messages,
812 inbox,
813 cut,
814 } = located;
815 cs.messages = messages;
816
817 let (inbox, active, lists) = if cut {
818 let none = Lists {
820 permissions: Vec::new(),
821 forms: Vec::new(),
822 };
823 (inbox, true, none)
824 } else {
825 let has_wait_route = self.op("experimental.session.wait").await.is_ok();
826 let end = {
827 let poll = self.poll_loop(&session, started, timeout_ms, poll_ms);
828 let child = async {
829 if has_wait_route {
830 self.child_loop(&session, &anchor, deadline_ms, poll_ms, &mut cs)
831 .await
832 } else {
833 std::future::pending().await
834 }
835 };
836 tokio::select! {
837 biased;
838 p = poll => match p {
839 PollEnd::Failed(e) => return Err(e),
840 PollEnd::Found(l) | PollEnd::Deadline(l) => SliceEnd::Poll(l),
841 },
842 c = child => SliceEnd::Child(c?),
843 }
844 };
845 match end {
846 SliceEnd::Child(done) => (
847 done.inbox,
848 done.active,
849 Lists {
850 permissions: Vec::new(),
851 forms: Vec::new(),
852 },
853 ),
854 SliceEnd::Poll(lists) => {
855 self.read_incremental(&session, &anchor, &mut cs.pages, &mut cs.messages)
858 .await?;
859 let (inbox, _, _) = self.list_op("session.inbox.list", &session).await?;
860 let active = self.is_active(&session).await?;
861 (rows(&inbox), active, lists)
862 }
863 }
864 };
865 let tokens = self.tokens_of(&session).await;
867
868 let snap = Snapshot {
869 messages: std::mem::take(&mut cs.messages),
870 inbox,
871 active,
872 permissions: lists.permissions,
873 forms: lists.forms,
874 now_ms: self.clock().now_ms(),
875 };
876 let mut ev = evaluate(&anchor, &snap, &self.settings);
877 if cut {
878 ev.state = WaitState::Running;
880 ev.outcome = None;
881 ev.error = None;
882 ev.concurrent_turn = None;
883 ev.turn_s = turn_start(&anchor, &snap, snap.now_ms).map(|s| seconds(snap.now_ms - s));
885 }
886 if ev.state == WaitState::Running && a.hard == Some(true) {
887 ev.state = WaitState::Timeout;
888 }
889
890 let parked_s = if snap.inbox.is_empty() {
891 None
892 } else {
893 let since = match anchor.source {
894 AnchorSource::Inbox { parked_since } if parked(&anchor, &snap) => {
895 Some(parked_since)
896 }
897 _ => snap.inbox.iter().filter_map(item_created).min(),
898 };
899 since.map(|s| seconds(snap.now_ms - s))
900 };
901 let (pending_permissions, more_permissions) =
902 ceiling(&snap.permissions, compact_permission);
903 let (pending_forms, more_forms) = ceiling(&snap.forms, compact_form);
904 let result = WaitResult {
905 state: ev.state,
906 elapsed_s: seconds(snap.now_ms - started),
907 turn_s: ev.turn_s,
908 inbox_pending: !snap.inbox.is_empty(),
909 tokens,
910 last_assistant_text: last_assistant_text(&anchor, &snap.messages),
911 pending_permissions,
912 pending_forms,
913 more_permissions,
914 more_forms,
915 verify_hint: (ev.state == WaitState::Idle).then(|| {
916 json!({
917 "tool": "session_changes",
918 "args": {"session_id": session, "from": anchor.id},
919 })
920 }),
921 outcome: ev.outcome,
922 concurrent_turn: ev.concurrent_turn,
923 error: ev.error,
924 parked_s,
925 pair,
926 };
927 Ok(serde_json::to_value(result).unwrap_or(Value::Null))
928 }
929}