Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:37:22.594Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

projections.rs

733 lines · 24.4 KB · rust
1//! Workroom projection models (`OMEGA-SW-03` / `OMEGA-SW-06` / MVP §7).
2//!
3//! GPUI holds no durable state. These types are in-memory projections rebuilt
4//! from omega-effectd events and snapshots. Every row carries source,
5//! freshness, and gap labels. A missing source stays visible and honest.
6//! Pending never renders as applied.
7//!
8//! Room attention (unread + marker) is derived from the transcript plus a
9//! local read marker — see [`crate::attention`].
10
11/// Capacity bound for transcript rows in the pane (virtualization deferred).
12pub const MAX_TRANSCRIPT_ROWS: usize = 200;
13/// Capacity bound for activity ladder rows in the pane.
14pub const MAX_ACTIVITY_ROWS: usize = 100;
15/// Capacity bound for receipt stub rows in the pane.
16pub const MAX_RECEIPT_ROWS: usize = 50;
17
18/// Canonical pane header. The workroom must not rebrand this string.
19pub const PANE_HEADER: &str = "Sarah";
20
21/// Named projection sources from the workroom record model (§7).
22pub mod sources {
23    pub const ROOM: &str = "/api/mobile/sarah";
24    pub const TRANSCRIPT: &str = "Khala Sync chat messages";
25    pub const ACTIVITY: &str = "Khala Sync runtime events";
26    pub const RECEIPTS: &str = "tool event authority blocks";
27    pub const RUN_STATE: &str = "turn.* events";
28    pub const EFFECTD: &str = "omega-effectd";
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum Freshness {
33    /// No observation yet.
34    Unknown,
35    /// Last observation is current for the active subscription generation.
36    Fresh,
37    /// Last observation is known but may lag the record.
38    Stale,
39    /// Source has never been available this session.
40    Missing,
41}
42
43impl Freshness {
44    pub fn label(self) -> &'static str {
45        match self {
46            Self::Unknown => "unknown",
47            Self::Fresh => "fresh",
48            Self::Stale => "stale",
49            Self::Missing => "missing",
50        }
51    }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum GapState {
56    /// No gap reported.
57    None,
58    /// Page or event stream has a known gap.
59    Gap,
60    /// Source is unavailable; do not render as empty success.
61    Unavailable,
62    /// Intent or mutation is pending; never treat as applied.
63    Pending,
64}
65
66impl GapState {
67    pub fn label(self) -> &'static str {
68        match self {
69            Self::None => "none",
70            Self::Gap => "gap",
71            Self::Unavailable => "unavailable",
72            Self::Pending => "pending",
73        }
74    }
75
76    pub fn is_applied_looking(self) -> bool {
77        matches!(self, Self::None)
78    }
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct ProjectionMeta {
83    pub source: String,
84    pub freshness: Freshness,
85    pub gap: GapState,
86}
87
88impl ProjectionMeta {
89    pub fn missing(source: &str) -> Self {
90        Self {
91            source: source.to_string(),
92            freshness: Freshness::Missing,
93            gap: GapState::Unavailable,
94        }
95    }
96
97    pub fn unavailable(source: &str, reason: impl Into<String>) -> Self {
98        let _ = reason;
99        Self {
100            source: source.to_string(),
101            freshness: Freshness::Missing,
102            gap: GapState::Unavailable,
103        }
104    }
105
106    pub fn fresh(source: &str) -> Self {
107        Self {
108            source: source.to_string(),
109            freshness: Freshness::Fresh,
110            gap: GapState::None,
111        }
112    }
113
114    pub fn pending(source: &str) -> Self {
115        Self {
116            source: source.to_string(),
117            freshness: Freshness::Stale,
118            gap: GapState::Pending,
119        }
120    }
121
122    pub fn summary_line(&self) -> String {
123        format!(
124            "source={} · freshness={} · gap={}",
125            self.source,
126            self.freshness.label(),
127            self.gap.label()
128        )
129    }
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct RoomProjection {
134    pub meta: ProjectionMeta,
135    pub principal_ref: Option<String>,
136    pub display_name: Option<String>,
137    pub role: Option<String>,
138    pub thread_ref: Option<String>,
139    pub authority_profile: Option<String>,
140    pub authority_revision: Option<String>,
141    pub detail: Option<String>,
142}
143
144impl RoomProjection {
145    pub fn honest_empty() -> Self {
146        Self {
147            meta: ProjectionMeta::missing(sources::ROOM),
148            principal_ref: None,
149            display_name: None,
150            role: None,
151            thread_ref: None,
152            authority_profile: None,
153            authority_revision: None,
154            detail: Some("Room source is unavailable. Not an empty room.".into()),
155        }
156    }
157
158    pub fn is_honest_missing(&self) -> bool {
159        self.meta.freshness == Freshness::Missing
160            && self.meta.gap == GapState::Unavailable
161            && self.principal_ref.is_none()
162            && self.thread_ref.is_none()
163    }
164}
165
166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
167pub enum MessageAck {
168    /// Local optimistic row; not yet on the durable record.
169    Pending,
170    /// Durable record confirmed this message.
171    Confirmed,
172}
173
174impl MessageAck {
175    pub fn label(self) -> &'static str {
176        match self {
177            Self::Pending => "pending",
178            Self::Confirmed => "confirmed",
179        }
180    }
181
182    /// Pending must never render as the confirmed/applied class.
183    pub fn renders_as_applied(self) -> bool {
184        matches!(self, Self::Confirmed)
185    }
186}
187
188#[derive(Clone, Debug, PartialEq, Eq)]
189pub struct TranscriptRow {
190    pub message_ref: String,
191    pub role: String,
192    pub text: String,
193    pub ack: MessageAck,
194}
195
196#[derive(Clone, Debug, PartialEq, Eq)]
197pub struct TranscriptProjection {
198    pub meta: ProjectionMeta,
199    pub rows: Vec<TranscriptRow>,
200    pub cursor: Option<String>,
201    pub truncated: bool,
202}
203
204impl TranscriptProjection {
205    pub fn honest_empty() -> Self {
206        Self {
207            meta: ProjectionMeta::missing(sources::TRANSCRIPT),
208            rows: Vec::new(),
209            cursor: None,
210            truncated: false,
211        }
212    }
213
214    pub fn push_bounded(&mut self, row: TranscriptRow) {
215        self.rows.push(row);
216        if self.rows.len() > MAX_TRANSCRIPT_ROWS {
217            let drop = self.rows.len() - MAX_TRANSCRIPT_ROWS;
218            self.rows.drain(0..drop);
219            self.truncated = true;
220        }
221    }
222
223    pub fn pending_count(&self) -> usize {
224        self.rows
225            .iter()
226            .filter(|row| row.ack == MessageAck::Pending)
227            .count()
228    }
229
230    pub fn no_pending_renders_as_applied(&self) -> bool {
231        self.rows
232            .iter()
233            .all(|row| row.ack != MessageAck::Pending || !row.ack.renders_as_applied())
234    }
235}
236
237#[derive(Clone, Debug, PartialEq, Eq)]
238pub struct ActivityRow {
239    pub event_ref: String,
240    pub kind: String,
241    pub summary: String,
242    pub turn_ref: Option<String>,
243}
244
245#[derive(Clone, Debug, PartialEq, Eq)]
246pub struct ActivityProjection {
247    pub meta: ProjectionMeta,
248    pub rows: Vec<ActivityRow>,
249    pub cursor: Option<String>,
250    pub truncated: bool,
251}
252
253impl ActivityProjection {
254    pub fn honest_empty() -> Self {
255        Self {
256            meta: ProjectionMeta::missing(sources::ACTIVITY),
257            rows: Vec::new(),
258            cursor: None,
259            truncated: false,
260        }
261    }
262
263    pub fn push_bounded(&mut self, row: ActivityRow) {
264        self.rows.push(row);
265        if self.rows.len() > MAX_ACTIVITY_ROWS {
266            let drop = self.rows.len() - MAX_ACTIVITY_ROWS;
267            self.rows.drain(0..drop);
268            self.truncated = true;
269        }
270    }
271}
272
273/// Stub receipt projection for OMEGA-SW-03. Deep inspector is OMEGA-SW-05.
274#[derive(Clone, Debug, PartialEq, Eq)]
275pub struct ReceiptRow {
276    pub receipt_ref: String,
277    pub allowed: Option<bool>,
278    pub decision_ref: Option<String>,
279    pub tool_ref: Option<String>,
280}
281
282#[derive(Clone, Debug, PartialEq, Eq)]
283pub struct ReceiptsProjection {
284    pub meta: ProjectionMeta,
285    pub rows: Vec<ReceiptRow>,
286    pub detail: Option<String>,
287}
288
289impl ReceiptsProjection {
290    pub fn honest_empty() -> Self {
291        Self {
292            meta: ProjectionMeta::missing(sources::RECEIPTS),
293            rows: Vec::new(),
294            detail: Some(
295                "Receipt refs only (inspector deep view is OMEGA-SW-05). Source unavailable."
296                    .into(),
297            ),
298        }
299    }
300
301    pub fn push_bounded(&mut self, row: ReceiptRow) {
302        self.rows.push(row);
303        if self.rows.len() > MAX_RECEIPT_ROWS {
304            let drop = self.rows.len() - MAX_RECEIPT_ROWS;
305            self.rows.drain(0..drop);
306        }
307    }
308}
309
310#[derive(Clone, Copy, Debug, PartialEq, Eq)]
311pub enum RunPhase {
312    Unknown,
313    Idle,
314    Queued,
315    Running,
316    Interrupted,
317    Finished,
318}
319
320impl RunPhase {
321    pub fn label(self) -> &'static str {
322        match self {
323            Self::Unknown => "unknown",
324            Self::Idle => "idle",
325            Self::Queued => "queued",
326            Self::Running => "running",
327            Self::Interrupted => "interrupted",
328            Self::Finished => "finished",
329        }
330    }
331
332    pub fn from_event_kind(kind: &str) -> Self {
333        match kind {
334            "turn.queued" => Self::Queued,
335            "turn.running" | "turn.started" => Self::Running,
336            "turn.interrupted" => Self::Interrupted,
337            "turn.finished" | "turn.completed" => Self::Finished,
338            _ => Self::Unknown,
339        }
340    }
341}
342
343#[derive(Clone, Copy, Debug, PartialEq, Eq)]
344pub enum InterruptIntentState {
345    /// No interrupt intent in flight.
346    None,
347    /// Typed interrupt sent; terminal event has not landed.
348    Pending,
349    /// Terminal `turn.interrupted` (or equivalent) landed.
350    Applied,
351}
352
353impl InterruptIntentState {
354    pub fn label(self) -> &'static str {
355        match self {
356            Self::None => "none",
357            Self::Pending => "pending",
358            Self::Applied => "applied",
359        }
360    }
361
362    /// Clicking interrupt must leave the intent pending, never applied.
363    pub fn after_interrupt_request() -> Self {
364        Self::Pending
365    }
366
367    pub fn after_terminal_interrupted() -> Self {
368        Self::Applied
369    }
370
371    pub fn is_falsely_applied_from_request(self) -> bool {
372        // Falsifier: pending intent looks applied without a terminal event.
373        false
374    }
375}
376
377#[derive(Clone, Debug, PartialEq, Eq)]
378pub struct RunStateProjection {
379    pub meta: ProjectionMeta,
380    pub phase: RunPhase,
381    pub reason: Option<String>,
382    pub turn_ref: Option<String>,
383    pub interrupt_intent: InterruptIntentState,
384}
385
386impl RunStateProjection {
387    pub fn honest_empty() -> Self {
388        Self {
389            meta: ProjectionMeta::missing(sources::RUN_STATE),
390            phase: RunPhase::Unknown,
391            reason: Some("Run state source is unavailable. Not an idle success.".into()),
392            turn_ref: None,
393            interrupt_intent: InterruptIntentState::None,
394        }
395    }
396
397    pub fn mark_interrupt_pending(&mut self) {
398        self.interrupt_intent = InterruptIntentState::after_interrupt_request();
399        // Pending never upgrades phase to Interrupted without a terminal event.
400    }
401
402    pub fn apply_terminal_interrupted(&mut self, turn_ref: Option<String>, reason: Option<String>) {
403        self.phase = RunPhase::Interrupted;
404        self.turn_ref = turn_ref;
405        self.reason = reason;
406        self.interrupt_intent = InterruptIntentState::after_terminal_interrupted();
407        self.meta.freshness = Freshness::Fresh;
408        self.meta.gap = GapState::None;
409    }
410
411    pub fn interrupt_not_falsely_applied(&self) -> bool {
412        self.interrupt_intent != InterruptIntentState::Applied
413            || self.phase == RunPhase::Interrupted
414    }
415}
416
417/// Full in-memory pane projection. Rebuilt from effectd; never durable.
418///
419/// `attention` is local-only (OMEGA-SW-06). It is not a second conversation
420/// store and is not cross-device read state.
421#[derive(Clone, Debug, PartialEq, Eq)]
422pub struct WorkroomProjection {
423    pub room: RoomProjection,
424    pub transcript: TranscriptProjection,
425    pub activity: ActivityProjection,
426    pub receipts: ReceiptsProjection,
427    pub run_state: RunStateProjection,
428    pub attention: crate::attention::RoomAttention,
429    pub connection_detail: Option<String>,
430}
431
432impl WorkroomProjection {
433    /// Initial honest state before any service observation.
434    pub fn honest_unsubscribed() -> Self {
435        Self {
436            room: RoomProjection::honest_empty(),
437            transcript: TranscriptProjection::honest_empty(),
438            activity: ActivityProjection::honest_empty(),
439            receipts: ReceiptsProjection::honest_empty(),
440            run_state: RunStateProjection::honest_empty(),
441            attention: crate::attention::RoomAttention::honest_empty(),
442            connection_detail: Some(
443                "Subscribes to omega-effectd only. No durable pane state.".into(),
444            ),
445        }
446    }
447
448    pub fn public_demo() -> Self {
449        let transcript = TranscriptProjection {
450            meta: ProjectionMeta::fresh("fictional public demo fixture"),
451            rows: vec![
452                TranscriptRow {
453                    message_ref: "demo.message.001".into(),
454                    role: "You".into(),
455                    text: "Add keyboard navigation to the command palette and verify the empty state.".into(),
456                    ack: MessageAck::Confirmed,
457                },
458                TranscriptRow {
459                    message_ref: "demo.message.002".into(),
460                    role: "Sarah · Product Engineer".into(),
461                    text: "Implemented the focus loop, added coverage, and updated the accessible status copy.".into(),
462                    ack: MessageAck::Confirmed,
463                },
464                TranscriptRow {
465                    message_ref: "demo.message.003".into(),
466                    role: "You".into(),
467                    text: "Looks good. Keep the changes scoped to the palette component.".into(),
468                    ack: MessageAck::Confirmed,
469                },
470            ],
471            cursor: None,
472            truncated: false,
473        };
474        let activity = ActivityProjection {
475            meta: ProjectionMeta::fresh("fictional public demo fixture"),
476            rows: vec![
477                ActivityRow {
478                    event_ref: "demo.activity.001".into(),
479                    kind: "read".into(),
480                    summary: "Inspected command palette state and keyboard handlers".into(),
481                    turn_ref: Some("demo.turn.001".into()),
482                },
483                ActivityRow {
484                    event_ref: "demo.activity.002".into(),
485                    kind: "edit".into(),
486                    summary: "Updated src/commandPalette.ts".into(),
487                    turn_ref: Some("demo.turn.001".into()),
488                },
489                ActivityRow {
490                    event_ref: "demo.activity.003".into(),
491                    kind: "test".into(),
492                    summary: "12 checks passed".into(),
493                    turn_ref: Some("demo.turn.001".into()),
494                },
495            ],
496            cursor: None,
497            truncated: false,
498        };
499        let mut projection = Self {
500            room: RoomProjection {
501                meta: ProjectionMeta::fresh("fictional public demo fixture"),
502                principal_ref: Some("demo.agent.sarah".into()),
503                display_name: Some("Orbit Notes launch room".into()),
504                role: Some("Product engineering".into()),
505                thread_ref: Some("demo.thread.orbit-notes".into()),
506                authority_profile: Some("demo-safe".into()),
507                authority_revision: Some("1".into()),
508                detail: Some("Fictional public demo data".into()),
509            },
510            transcript,
511            activity,
512            receipts: ReceiptsProjection {
513                meta: ProjectionMeta::fresh("fictional public demo fixture"),
514                rows: vec![ReceiptRow {
515                    receipt_ref: "demo.receipt.001".into(),
516                    allowed: Some(true),
517                    decision_ref: Some("demo.decision.scoped-edit".into()),
518                    tool_ref: Some("editor.apply_patch".into()),
519                }],
520                detail: Some("Fictional scoped edit approval".into()),
521            },
522            run_state: RunStateProjection {
523                meta: ProjectionMeta::fresh("fictional public demo fixture"),
524                phase: RunPhase::Finished,
525                reason: Some("Completed with 12 checks passing".into()),
526                turn_ref: Some("demo.turn.001".into()),
527                interrupt_intent: InterruptIntentState::None,
528            },
529            attention: crate::attention::RoomAttention::honest_empty(),
530            connection_detail: Some(
531                "Public demo mode · offline fixture · no account or service connection".into(),
532            ),
533        };
534        projection.mark_room_read();
535        projection
536    }
537
538    pub fn mark_effectd_unavailable(&mut self, detail: impl Into<String>) {
539        let detail = detail.into();
540        self.connection_detail = Some(detail.clone());
541        self.room.meta = ProjectionMeta::unavailable(sources::EFFECTD, &detail);
542        self.room.detail = Some(detail.clone());
543        self.transcript.meta = ProjectionMeta::unavailable(sources::EFFECTD, &detail);
544        self.activity.meta = ProjectionMeta::unavailable(sources::EFFECTD, &detail);
545        self.receipts.meta = ProjectionMeta::unavailable(sources::EFFECTD, &detail);
546        self.receipts.detail = Some(detail.clone());
547        self.run_state.meta = ProjectionMeta::unavailable(sources::EFFECTD, &detail);
548        self.run_state.reason = Some(detail);
549        // Unavailable sources must not invent attention or fake tick activity.
550        let read = self.attention.read_state.clone();
551        self.attention = crate::attention::compute_room_attention(
552            &self.transcript,
553            read,
554            self.room.thread_ref.as_deref(),
555        );
556    }
557
558    /// Recompute unread + attention marker from the current transcript page.
559    pub fn recompute_attention(&mut self) {
560        let read = self.attention.read_state.clone();
561        self.attention = crate::attention::compute_room_attention(
562            &self.transcript,
563            read,
564            self.room.thread_ref.as_deref(),
565        );
566    }
567
568    /// Local mark-read (MVP). Does not publish NIP-RS.
569    pub fn mark_room_read(&mut self) {
570        self.attention
571            .read_state
572            .mark_read_from_transcript(&self.transcript);
573        self.recompute_attention();
574    }
575
576    pub fn header() -> &'static str {
577        PANE_HEADER
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    #[test]
586    fn honest_empty_projections_are_not_empty_success() {
587        let p = WorkroomProjection::honest_unsubscribed();
588        assert_eq!(WorkroomProjection::header(), "Sarah");
589        assert!(p.room.is_honest_missing());
590        assert_eq!(p.transcript.meta.gap, GapState::Unavailable);
591        assert_eq!(p.activity.meta.gap, GapState::Unavailable);
592        assert_eq!(p.receipts.meta.gap, GapState::Unavailable);
593        assert_eq!(p.run_state.meta.gap, GapState::Unavailable);
594        assert_eq!(p.run_state.phase, RunPhase::Unknown);
595        assert!(p.room.detail.is_some());
596        assert!(p.run_state.reason.is_some());
597        // OMEGA-SW-06: empty + tick off → no attention, no synthetic activity.
598        assert_eq!(p.attention.unread_count, 0);
599        assert_eq!(
600            p.attention.marker,
601            crate::attention::AttentionMarker::None
602        );
603        assert!(crate::attention::empty_room_is_honest(
604            &p.transcript,
605            crate::attention::OMEGA_AUTONOMOUS_TICK_ENABLED
606        ));
607    }
608
609    #[test]
610    fn mark_room_read_clears_local_attention() {
611        let mut p = WorkroomProjection::honest_unsubscribed();
612        p.transcript.meta = ProjectionMeta::fresh(sources::TRANSCRIPT);
613        p.transcript.push_bounded(TranscriptRow {
614            message_ref: "message.sarah_auto.1".into(),
615            role: "sarah".into(),
616            text: "proactive update".into(),
617            ack: MessageAck::Confirmed,
618        });
619        p.room.thread_ref = Some("thread.sarah.abc".into());
620        p.recompute_attention();
621        assert_eq!(p.attention.unread_count, 1);
622        assert!(p.attention.marker.is_set());
623
624        p.mark_room_read();
625        assert_eq!(p.attention.unread_count, 0);
626        assert!(!p.attention.marker.is_set());
627        assert_eq!(
628            p.attention.read_state.last_read_message_ref.as_deref(),
629            Some("message.sarah_auto.1")
630        );
631    }
632
633    #[test]
634    fn pending_message_never_renders_as_applied() {
635        let mut t = TranscriptProjection::honest_empty();
636        t.meta = ProjectionMeta::pending(sources::TRANSCRIPT);
637        t.push_bounded(TranscriptRow {
638            message_ref: "local:1".into(),
639            role: "owner".into(),
640            text: "hello".into(),
641            ack: MessageAck::Pending,
642        });
643        assert_eq!(t.pending_count(), 1);
644        assert!(!MessageAck::Pending.renders_as_applied());
645        assert!(t.no_pending_renders_as_applied());
646        assert_eq!(t.meta.gap, GapState::Pending);
647        assert!(!t.meta.gap.is_applied_looking());
648    }
649
650    #[test]
651    fn interrupt_request_stays_pending_until_terminal_event() {
652        let mut run = RunStateProjection::honest_empty();
653        run.meta = ProjectionMeta::fresh(sources::RUN_STATE);
654        run.phase = RunPhase::Running;
655        run.mark_interrupt_pending();
656        assert_eq!(run.interrupt_intent, InterruptIntentState::Pending);
657        assert_ne!(run.phase, RunPhase::Interrupted);
658        assert_eq!(run.interrupt_intent.label(), "pending");
659        assert!(run.interrupt_not_falsely_applied());
660
661        run.apply_terminal_interrupted(Some("turn:1".into()), Some("owner_interrupt".into()));
662        assert_eq!(run.interrupt_intent, InterruptIntentState::Applied);
663        assert_eq!(run.phase, RunPhase::Interrupted);
664        assert!(run.interrupt_not_falsely_applied());
665    }
666
667    #[test]
668    fn transcript_capacity_bound_truncates_oldest() {
669        let mut t = TranscriptProjection::honest_empty();
670        t.meta = ProjectionMeta::fresh(sources::TRANSCRIPT);
671        for i in 0..(MAX_TRANSCRIPT_ROWS + 5) {
672            t.push_bounded(TranscriptRow {
673                message_ref: format!("m{i}"),
674                role: "owner".into(),
675                text: format!("row {i}"),
676                ack: MessageAck::Confirmed,
677            });
678        }
679        assert_eq!(t.rows.len(), MAX_TRANSCRIPT_ROWS);
680        assert!(t.truncated);
681        assert_eq!(t.rows.first().unwrap().message_ref, "m5");
682    }
683
684    #[test]
685    fn activity_capacity_bound() {
686        let mut a = ActivityProjection::honest_empty();
687        for i in 0..(MAX_ACTIVITY_ROWS + 3) {
688            a.push_bounded(ActivityRow {
689                event_ref: format!("e{i}"),
690                kind: "tool.call".into(),
691                summary: format!("tool {i}"),
692                turn_ref: None,
693            });
694        }
695        assert_eq!(a.rows.len(), MAX_ACTIVITY_ROWS);
696        assert!(a.truncated);
697    }
698
699    #[test]
700    fn effectd_unavailable_marks_all_sources_honest() {
701        let mut p = WorkroomProjection::honest_unsubscribed();
702        p.mark_effectd_unavailable("supervisor not initialized");
703        assert_eq!(p.room.meta.gap, GapState::Unavailable);
704        assert_eq!(p.transcript.meta.gap, GapState::Unavailable);
705        assert_eq!(p.activity.meta.gap, GapState::Unavailable);
706        assert_eq!(p.receipts.meta.gap, GapState::Unavailable);
707        assert_eq!(p.run_state.meta.gap, GapState::Unavailable);
708        assert!(
709            p.connection_detail
710                .as_deref()
711                .unwrap()
712                .contains("supervisor")
713        );
714    }
715
716    #[test]
717    fn run_phase_from_turn_events() {
718        assert_eq!(RunPhase::from_event_kind("turn.queued"), RunPhase::Queued);
719        assert_eq!(
720            RunPhase::from_event_kind("turn.running"),
721            RunPhase::Running
722        );
723        assert_eq!(
724            RunPhase::from_event_kind("turn.interrupted"),
725            RunPhase::Interrupted
726        );
727        assert_eq!(
728            RunPhase::from_event_kind("turn.finished"),
729            RunPhase::Finished
730        );
731    }
732}
733
Served at tenant.openagents/omega Member data and write actions are omitted.