1use anyhow::{Context, Result};
16pub use rusqlite::Connection;
17use rusqlite::{OptionalExtension, params};
18use serde::{Deserialize, Serialize};
19use std::collections::BTreeMap;
20use std::path::{Path, PathBuf};
21use std::time::{SystemTime, UNIX_EPOCH};
22
23const STATE_DB_FILE: &str = "state.db";
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum ActorState {
32 Starting,
33 Ready,
34 Busy,
35 WaitingInput,
36 Closed,
37 Blocked,
38}
39
40impl ActorState {
41 pub fn as_str(self) -> &'static str {
42 match self {
43 Self::Starting => "starting",
44 Self::Ready => "ready",
45 Self::Busy => "busy",
46 Self::WaitingInput => "waiting_input",
47 Self::Closed => "closed",
48 Self::Blocked => "blocked",
49 }
50 }
51
52 pub fn parse(raw: &str) -> Option<Self> {
53 match raw {
54 "starting" => Some(Self::Starting),
55 "ready" => Some(Self::Ready),
56 "busy" => Some(Self::Busy),
57 "waiting_input" => Some(Self::WaitingInput),
58 "closed" => Some(Self::Closed),
59 "blocked" => Some(Self::Blocked),
60 _ => None,
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct ActorLastTransition {
67 pub caller: String,
68 pub reason: String,
69 pub timestamp: u64,
70 pub prior_generation: u64,
71 pub new_generation: u64,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct ActorRecord {
76 pub document_id: String,
77 pub session_id: String,
78 pub generation: u64,
79 pub pane_id: String,
80 pub window_id: String,
81 pub harness: String,
82 pub state: ActorState,
83 pub last_transition: ActorLastTransition,
84}
85
86#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
91pub struct ActorTransitionStatus {
92 pub prior_generation: u64,
93 pub new_generation: u64,
94 pub caller: String,
95 pub reason: String,
96 pub old_pane: Option<String>,
97 pub new_pane: String,
98 pub old_window: Option<String>,
99 pub new_window: Option<String>,
100 pub timestamp: u64,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
104pub struct SupervisorLeaseStatus {
105 pub generation: u64,
106 pub supervisor_pid: Option<u32>,
107 pub supervisor_socket: Option<String>,
108 pub last_heartbeat: Option<u64>,
109 pub runtime_state: Option<String>,
110}
111
112#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
113pub struct DispatchAttemptStatus {
114 pub receipt_id: u64,
115 pub generation: u64,
116 pub command_kind: String,
117 pub accepted_stage: Option<String>,
118 pub failed_stage: Option<String>,
119 pub diagnostic_payload: Option<String>,
120 pub result_status: Option<String>,
121 pub proof_scope: Option<String>,
122 pub dispatch_start_proven: bool,
123 pub timestamp: u64,
124}
125
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub struct DispatchAttemptInsert<'a> {
128 pub document_id: &'a str,
129 pub generation: u64,
130 pub command_kind: &'a str,
131 pub accepted_stage: Option<&'a str>,
132 pub failed_stage: Option<&'a str>,
133 pub diagnostic_payload: &'a str,
134 pub result_status: &'a str,
135 pub proof_scope: &'a str,
136 pub dispatch_start_proven: bool,
137}
138
139#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
140pub struct ProjectionDiagnosticStatus {
141 pub projection: String,
142 pub message: String,
143 pub source_generation: Option<u64>,
144 pub intended_hash: Option<String>,
145 pub retry_status: Option<String>,
146 pub timestamp: u64,
147}
148
149#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
150pub struct QueueHeadStatus {
151 pub document_id: String,
152 pub queue_name: String,
153 pub generation: Option<u64>,
154 pub head_id: Option<String>,
155 pub prompt: String,
156 pub state: String,
157 pub priority: u64,
158 pub selected_at: u64,
159 pub updated_at: u64,
160}
161
162#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
163pub struct QueueControlStatus {
164 pub receipt_id: u64,
165 pub scope_kind: String,
166 pub scope_id: String,
167 pub state: String,
168 pub reason: Option<String>,
169 pub operation_receipt_id: Option<u64>,
170 pub updated_at: u64,
171}
172
173#[derive(Clone, Debug, PartialEq, Eq)]
174pub struct QueueControlInsert<'a> {
175 pub scope_kind: &'a str,
176 pub scope_id: &'a str,
177 pub state: &'a str,
178 pub reason: Option<&'a str>,
179 pub operation_receipt_id: Option<u64>,
180}
181
182#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
183pub struct QueueBackpressureStatus {
184 pub receipt_id: u64,
185 pub document_id: String,
186 pub generation: Option<u64>,
187 pub command_kind: String,
188 pub capacity_class: String,
189 pub reason: String,
190 pub dispatch_receipt_id: Option<u64>,
191 pub timestamp: u64,
192}
193
194#[derive(Clone, Debug, PartialEq, Eq)]
195pub struct QueueBackpressureInsert<'a> {
196 pub document_id: &'a str,
197 pub generation: Option<u64>,
198 pub command_kind: &'a str,
199 pub capacity_class: &'a str,
200 pub reason: &'a str,
201 pub dispatch_receipt_id: Option<u64>,
202}
203
204#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
205pub struct AdminOperationStatus {
206 pub receipt_id: u64,
207 pub operation_kind: String,
208 pub document_id: Option<String>,
209 pub status: String,
210 pub diagnostic_payload: Option<String>,
211 pub timestamp: u64,
212}
213
214#[derive(Clone, Debug, PartialEq, Eq)]
215pub struct ProjectionDiagnosticInsert<'a> {
216 pub projection: &'a str,
217 pub document_id: &'a str,
218 pub message: &'a str,
219 pub source_generation: Option<u64>,
220 pub intended_hash: Option<&'a str>,
221 pub retry_status: &'a str,
222}
223
224#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
225pub struct SessionOperatorStatus {
226 pub record: Option<ActorRecord>,
227 pub transitions: Vec<ActorTransitionStatus>,
228 pub supervisor_lease: Option<SupervisorLeaseStatus>,
229 pub dispatch_attempts: Vec<DispatchAttemptStatus>,
230 pub projection_diagnostics: Vec<ProjectionDiagnosticStatus>,
231}
232
233#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
234pub struct ControlPlaneStoreCounts {
235 pub actor_documents: usize,
236 pub live_actor_documents: usize,
237 pub actor_transitions: usize,
238 pub supervisor_leases: usize,
239 pub state_events: usize,
240 pub dispatch_receipts: usize,
241 pub queue_heads: usize,
242 pub document_cycles: usize,
243 pub pending_mutations: usize,
244 pub projection_diagnostics: usize,
245 pub admin_operations: usize,
246 pub queue_controls: usize,
247 pub queue_backpressure: usize,
248 pub crash_recovery_markers: usize,
249 pub layout_states: usize,
250}
251
252impl ControlPlaneStoreCounts {
253 pub fn total_authoritative_rows(&self) -> usize {
254 self.actor_documents
255 + self.actor_transitions
256 + self.supervisor_leases
257 + self.state_events
258 + self.dispatch_receipts
259 + self.queue_heads
260 + self.document_cycles
261 + self.pending_mutations
262 + self.projection_diagnostics
263 + self.admin_operations
264 + self.queue_controls
265 + self.queue_backpressure
266 + self.crash_recovery_markers
267 + self.layout_states
268 }
269}
270
271#[derive(Clone, Debug, PartialEq, Eq)]
272pub struct SessionActorCloseoutMutation<'a> {
273 pub item_id: &'a str,
274 pub mutation_kind: &'a str,
275 pub status: &'a str,
276}
277
278#[derive(Clone, Debug, PartialEq, Eq)]
279pub struct SessionActorCloseoutCommit<'a> {
280 pub document_id: &'a str,
281 pub cycle_id: &'a str,
282 pub cycle_state: &'a str,
283 pub queue_name: &'a str,
284 pub queue_head_id: Option<&'a str>,
285 pub queue_head_prompt: Option<&'a str>,
286 pub queue_head_state: &'a str,
287 pub response_commit: Option<&'a str>,
288 pub mutations: Vec<SessionActorCloseoutMutation<'a>>,
289}
290
291#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
292pub struct StateEventStatus {
293 pub sequence: u64,
294 pub event_id: String,
295 pub document_hash: String,
296 pub domain: String,
297 pub fact_type: String,
298 pub payload_json: String,
299 pub timestamp: u64,
300}
301
302#[derive(Clone, Debug, PartialEq, Eq)]
303pub struct StateEventInsert<'a> {
304 pub event_id: &'a str,
305 pub document_hash: &'a str,
306 pub domain: &'a str,
307 pub fact_type: &'a str,
308 pub payload_json: &'a str,
309}
310
311pub fn state_db_path(project_root: &Path) -> PathBuf {
316 project_root.join(".agent-doc").join(STATE_DB_FILE)
317}
318
319pub fn open_state_db(project_root: &Path) -> Result<Connection> {
320 let path = state_db_path(project_root);
321 if let Some(parent) = path.parent() {
322 std::fs::create_dir_all(parent)?;
323 }
324 let conn =
325 Connection::open(&path).with_context(|| format!("failed to open {}", path.display()))?;
326 initialize_state_db(&conn)?;
327 Ok(conn)
328}
329
330pub fn initialize_state_db(conn: &Connection) -> Result<()> {
331 conn.execute_batch(
332 r#"
333 PRAGMA foreign_keys = ON;
334 PRAGMA journal_mode = WAL;
335 PRAGMA busy_timeout = 5000;
336
337 CREATE TABLE IF NOT EXISTS documents (
338 document_id TEXT PRIMARY KEY,
339 canonical_path TEXT NOT NULL,
340 session_id TEXT NOT NULL,
341 generation INTEGER NOT NULL,
342 pane_id TEXT NOT NULL,
343 window_id TEXT NOT NULL,
344 harness_id TEXT NOT NULL,
345 actor_state TEXT NOT NULL,
346 launch_mode TEXT,
347 controller_epoch INTEGER,
348 last_transition_id INTEGER
349 );
350
351 CREATE TABLE IF NOT EXISTS actor_transitions (
352 id INTEGER PRIMARY KEY AUTOINCREMENT,
353 document_id TEXT NOT NULL,
354 prior_generation INTEGER NOT NULL,
355 new_generation INTEGER NOT NULL,
356 caller TEXT NOT NULL,
357 reason TEXT NOT NULL,
358 old_pane TEXT,
359 new_pane TEXT NOT NULL,
360 old_window TEXT,
361 new_window TEXT,
362 timestamp INTEGER NOT NULL
363 );
364
365 CREATE TABLE IF NOT EXISTS supervisor_leases (
366 document_id TEXT NOT NULL,
367 generation INTEGER NOT NULL,
368 supervisor_pid INTEGER,
369 supervisor_socket TEXT,
370 last_heartbeat INTEGER,
371 runtime_state TEXT,
372 PRIMARY KEY (document_id, generation)
373 );
374
375 CREATE TABLE IF NOT EXISTS state_events (
376 id INTEGER PRIMARY KEY AUTOINCREMENT,
377 event_id TEXT NOT NULL UNIQUE,
378 document_hash TEXT NOT NULL,
379 domain TEXT NOT NULL,
380 fact_type TEXT NOT NULL,
381 payload_json TEXT NOT NULL,
382 timestamp INTEGER NOT NULL
383 );
384
385 CREATE INDEX IF NOT EXISTS state_events_document_hash_id
386 ON state_events(document_hash, id);
387
388 CREATE TABLE IF NOT EXISTS dispatch_attempts (
389 id INTEGER PRIMARY KEY AUTOINCREMENT,
390 document_id TEXT NOT NULL,
391 generation INTEGER NOT NULL,
392 command_kind TEXT NOT NULL,
393 accepted_stage TEXT,
394 failed_stage TEXT,
395 diagnostic_payload TEXT,
396 result_status TEXT,
397 proof_scope TEXT,
398 dispatch_start_proven INTEGER NOT NULL DEFAULT 0,
399 timestamp INTEGER NOT NULL
400 );
401
402 CREATE TABLE IF NOT EXISTS projection_diagnostics (
403 id INTEGER PRIMARY KEY AUTOINCREMENT,
404 projection TEXT NOT NULL,
405 document_id TEXT NOT NULL,
406 message TEXT NOT NULL,
407 timestamp INTEGER NOT NULL
408 );
409
410 CREATE TABLE IF NOT EXISTS queue_heads (
411 document_id TEXT NOT NULL,
412 queue_name TEXT NOT NULL,
413 generation INTEGER,
414 head_id TEXT,
415 prompt TEXT NOT NULL,
416 state TEXT NOT NULL,
417 priority INTEGER NOT NULL DEFAULT 0,
418 selected_at INTEGER NOT NULL,
419 updated_at INTEGER NOT NULL,
420 PRIMARY KEY (document_id, queue_name)
421 );
422
423 CREATE TABLE IF NOT EXISTS queue_controls (
424 id INTEGER PRIMARY KEY AUTOINCREMENT,
425 scope_kind TEXT NOT NULL,
426 scope_id TEXT NOT NULL,
427 state TEXT NOT NULL,
428 reason TEXT,
429 operation_receipt_id INTEGER,
430 updated_at INTEGER NOT NULL,
431 UNIQUE(scope_kind, scope_id)
432 );
433
434 CREATE TABLE IF NOT EXISTS queue_backpressure (
435 id INTEGER PRIMARY KEY AUTOINCREMENT,
436 document_id TEXT NOT NULL,
437 generation INTEGER,
438 command_kind TEXT NOT NULL,
439 capacity_class TEXT NOT NULL,
440 reason TEXT NOT NULL,
441 dispatch_receipt_id INTEGER,
442 timestamp INTEGER NOT NULL
443 );
444
445 CREATE TABLE IF NOT EXISTS document_cycles (
446 document_id TEXT NOT NULL,
447 cycle_id TEXT NOT NULL,
448 state TEXT NOT NULL,
449 queue_head_id TEXT,
450 response_commit TEXT,
451 updated_at INTEGER NOT NULL,
452 PRIMARY KEY (document_id, cycle_id)
453 );
454
455 CREATE TABLE IF NOT EXISTS pending_mutations (
456 id INTEGER PRIMARY KEY AUTOINCREMENT,
457 document_id TEXT NOT NULL,
458 cycle_id TEXT NOT NULL,
459 item_id TEXT NOT NULL,
460 mutation_kind TEXT NOT NULL,
461 status TEXT NOT NULL,
462 recorded_at INTEGER NOT NULL,
463 UNIQUE(document_id, cycle_id, item_id, mutation_kind)
464 );
465
466 CREATE TABLE IF NOT EXISTS admin_operations (
467 id INTEGER PRIMARY KEY AUTOINCREMENT,
468 operation_kind TEXT NOT NULL,
469 document_id TEXT,
470 status TEXT NOT NULL,
471 diagnostic_payload TEXT,
472 timestamp INTEGER NOT NULL
473 );
474
475 CREATE TABLE IF NOT EXISTS crash_recovery_markers (
476 id INTEGER PRIMARY KEY AUTOINCREMENT,
477 marker_kind TEXT NOT NULL,
478 document_id TEXT,
479 generation INTEGER,
480 status TEXT NOT NULL,
481 diagnostic_payload TEXT,
482 timestamp INTEGER NOT NULL
483 );
484
485 CREATE TABLE IF NOT EXISTS layout_states (
486 scope TEXT PRIMARY KEY,
487 columns_json TEXT NOT NULL,
488 updated_at INTEGER NOT NULL
489 );
490 "#,
491 )?;
492 ensure_dispatch_attempt_receipt_columns(conn)?;
493 ensure_projection_diagnostic_columns(conn)?;
494 ensure_queue_head_columns(conn)?;
495 Ok(())
496}
497
498fn ensure_column(conn: &Connection, table: &str, column: &str, definition: &str) -> Result<()> {
499 let mut stmt = conn.prepare(&format!("PRAGMA table_info({table})"))?;
500 let mut rows = stmt.query([])?;
501 while let Some(row) = rows.next()? {
502 let existing: String = row.get(1)?;
503 if existing == column {
504 return Ok(());
505 }
506 }
507 conn.execute(&format!("ALTER TABLE {table} ADD COLUMN {definition}"), [])?;
508 Ok(())
509}
510
511fn ensure_dispatch_attempt_receipt_columns(conn: &Connection) -> Result<()> {
512 ensure_column(
513 conn,
514 "dispatch_attempts",
515 "result_status",
516 "result_status TEXT",
517 )?;
518 ensure_column(conn, "dispatch_attempts", "proof_scope", "proof_scope TEXT")?;
519 ensure_column(
520 conn,
521 "dispatch_attempts",
522 "dispatch_start_proven",
523 "dispatch_start_proven INTEGER NOT NULL DEFAULT 0",
524 )?;
525 Ok(())
526}
527
528fn ensure_projection_diagnostic_columns(conn: &Connection) -> Result<()> {
529 ensure_column(
530 conn,
531 "projection_diagnostics",
532 "source_generation",
533 "source_generation INTEGER",
534 )?;
535 ensure_column(
536 conn,
537 "projection_diagnostics",
538 "intended_hash",
539 "intended_hash TEXT",
540 )?;
541 ensure_column(
542 conn,
543 "projection_diagnostics",
544 "retry_status",
545 "retry_status TEXT",
546 )?;
547 Ok(())
548}
549
550fn ensure_queue_head_columns(conn: &Connection) -> Result<()> {
551 ensure_column(conn, "queue_heads", "generation", "generation INTEGER")?;
552 ensure_column(
553 conn,
554 "queue_heads",
555 "priority",
556 "priority INTEGER NOT NULL DEFAULT 0",
557 )?;
558 Ok(())
559}
560
561pub fn sqlite_i64(value: u64, name: &str) -> Result<i64> {
566 i64::try_from(value).with_context(|| format!("{name} is too large for sqlite INTEGER"))
567}
568
569pub fn sqlite_u64(value: i64, name: &str) -> Result<u64> {
570 u64::try_from(value).with_context(|| format!("{name} is negative in sqlite state"))
571}
572
573pub fn timestamp_secs() -> u64 {
574 SystemTime::now()
575 .duration_since(UNIX_EPOCH)
576 .unwrap_or_default()
577 .as_secs()
578}
579
580fn count_rows(conn: &Connection, sql: &str, label: &str) -> Result<usize> {
581 let count: i64 = conn
582 .query_row(sql, [], |row| row.get(0))
583 .with_context(|| format!("failed to count {label} in controller state"))?;
584 usize::try_from(count).with_context(|| format!("{label} count is negative"))
585}
586
587pub fn load_control_plane_store_counts(conn: &Connection) -> Result<ControlPlaneStoreCounts> {
588 Ok(ControlPlaneStoreCounts {
589 actor_documents: count_rows(conn, "SELECT COUNT(*) FROM documents", "actor documents")?,
590 live_actor_documents: count_rows(
591 conn,
592 "SELECT COUNT(*) FROM documents WHERE actor_state != 'closed'",
593 "live actor documents",
594 )?,
595 actor_transitions: count_rows(
596 conn,
597 "SELECT COUNT(*) FROM actor_transitions",
598 "actor transitions",
599 )?,
600 supervisor_leases: count_rows(
601 conn,
602 "SELECT COUNT(*) FROM supervisor_leases",
603 "supervisor leases",
604 )?,
605 state_events: count_rows(conn, "SELECT COUNT(*) FROM state_events", "state events")?,
606 dispatch_receipts: count_rows(
607 conn,
608 "SELECT COUNT(*) FROM dispatch_attempts",
609 "dispatch attempts",
610 )?,
611 queue_heads: count_rows(conn, "SELECT COUNT(*) FROM queue_heads", "queue heads")?,
612 document_cycles: count_rows(
613 conn,
614 "SELECT COUNT(*) FROM document_cycles",
615 "document cycles",
616 )?,
617 pending_mutations: count_rows(
618 conn,
619 "SELECT COUNT(*) FROM pending_mutations",
620 "pending mutations",
621 )?,
622 projection_diagnostics: count_rows(
623 conn,
624 "SELECT COUNT(*) FROM projection_diagnostics",
625 "projection diagnostics",
626 )?,
627 admin_operations: count_rows(
628 conn,
629 "SELECT COUNT(*) FROM admin_operations",
630 "admin operations",
631 )?,
632 queue_controls: count_rows(
633 conn,
634 "SELECT COUNT(*) FROM queue_controls",
635 "queue controls",
636 )?,
637 queue_backpressure: count_rows(
638 conn,
639 "SELECT COUNT(*) FROM queue_backpressure",
640 "queue backpressure",
641 )?,
642 crash_recovery_markers: count_rows(
643 conn,
644 "SELECT COUNT(*) FROM crash_recovery_markers",
645 "crash recovery markers",
646 )?,
647 layout_states: count_rows(conn, "SELECT COUNT(*) FROM layout_states", "layout states")?,
648 })
649}
650
651pub fn actor_record_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<ActorRecord> {
656 let actor_state: String = row.get("actor_state")?;
657 let state = ActorState::parse(&actor_state).ok_or(rusqlite::Error::InvalidQuery)?;
658 let generation: i64 = row.get("generation")?;
659 let transition_prior: i64 = row.get("prior_generation")?;
660 let transition_new: i64 = row.get("new_generation")?;
661 let transition_timestamp: i64 = row.get("timestamp")?;
662 Ok(ActorRecord {
663 document_id: row.get("document_id")?,
664 session_id: row.get("session_id")?,
665 generation: sqlite_u64(generation, "generation")
666 .map_err(|_| rusqlite::Error::InvalidQuery)?,
667 pane_id: row.get("pane_id")?,
668 window_id: row.get("window_id")?,
669 harness: row.get("harness_id")?,
670 state,
671 last_transition: ActorLastTransition {
672 caller: row.get("caller")?,
673 reason: row.get("reason")?,
674 timestamp: sqlite_u64(transition_timestamp, "transition timestamp")
675 .map_err(|_| rusqlite::Error::InvalidQuery)?,
676 prior_generation: sqlite_u64(transition_prior, "transition prior_generation")
677 .map_err(|_| rusqlite::Error::InvalidQuery)?,
678 new_generation: sqlite_u64(transition_new, "transition new_generation")
679 .map_err(|_| rusqlite::Error::InvalidQuery)?,
680 },
681 })
682}
683
684pub fn load_actor_record_from_db(
685 conn: &Connection,
686 document_id: &str,
687) -> Result<Option<ActorRecord>> {
688 conn.query_row(
689 r#"
690 SELECT
691 d.document_id,
692 d.session_id,
693 d.generation,
694 d.pane_id,
695 d.window_id,
696 d.harness_id,
697 d.actor_state,
698 t.caller,
699 t.reason,
700 t.timestamp,
701 t.prior_generation,
702 t.new_generation
703 FROM documents d
704 JOIN actor_transitions t ON t.id = d.last_transition_id
705 WHERE d.document_id = ?1
706 "#,
707 params![document_id],
708 actor_record_from_row,
709 )
710 .optional()
711 .context("failed to load actor record from controller state")
712}
713
714pub fn load_actor_store_from_db(conn: &Connection) -> Result<BTreeMap<String, ActorRecord>> {
715 let mut stmt = conn.prepare(
716 r#"
717 SELECT
718 d.document_id,
719 d.session_id,
720 d.generation,
721 d.pane_id,
722 d.window_id,
723 d.harness_id,
724 d.actor_state,
725 t.caller,
726 t.reason,
727 t.timestamp,
728 t.prior_generation,
729 t.new_generation
730 FROM documents d
731 JOIN actor_transitions t ON t.id = d.last_transition_id
732 ORDER BY d.document_id
733 "#,
734 )?;
735 let mut store = BTreeMap::new();
736 for row in stmt.query_map([], actor_record_from_row)? {
737 let record = row?;
738 store.insert(record.document_id.clone(), record);
739 }
740 Ok(store)
741}
742
743pub fn load_actor_transitions_from_db(
744 conn: &Connection,
745 document_id: &str,
746) -> Result<Vec<ActorTransitionStatus>> {
747 let mut stmt = conn.prepare(
748 r#"
749 SELECT
750 prior_generation,
751 new_generation,
752 caller,
753 reason,
754 old_pane,
755 new_pane,
756 old_window,
757 new_window,
758 timestamp
759 FROM actor_transitions
760 WHERE document_id = ?1
761 ORDER BY id
762 "#,
763 )?;
764 let mut transitions = Vec::new();
765 for row in stmt.query_map(params![document_id], |row| {
766 let prior_generation: i64 = row.get("prior_generation")?;
767 let new_generation: i64 = row.get("new_generation")?;
768 let timestamp: i64 = row.get("timestamp")?;
769 Ok(ActorTransitionStatus {
770 prior_generation: sqlite_u64(prior_generation, "transition prior_generation")
771 .map_err(|_| rusqlite::Error::InvalidQuery)?,
772 new_generation: sqlite_u64(new_generation, "transition new_generation")
773 .map_err(|_| rusqlite::Error::InvalidQuery)?,
774 caller: row.get("caller")?,
775 reason: row.get("reason")?,
776 old_pane: row.get("old_pane")?,
777 new_pane: row.get("new_pane")?,
778 old_window: row.get("old_window")?,
779 new_window: row.get("new_window")?,
780 timestamp: sqlite_u64(timestamp, "transition timestamp")
781 .map_err(|_| rusqlite::Error::InvalidQuery)?,
782 })
783 })? {
784 transitions.push(row?);
785 }
786 Ok(transitions)
787}
788
789pub fn load_supervisor_lease_from_db(
790 conn: &Connection,
791 document_id: &str,
792 generation: u64,
793) -> Result<Option<SupervisorLeaseStatus>> {
794 conn.query_row(
795 r#"
796 SELECT
797 generation,
798 supervisor_pid,
799 supervisor_socket,
800 last_heartbeat,
801 runtime_state
802 FROM supervisor_leases
803 WHERE document_id = ?1 AND generation = ?2
804 "#,
805 params![document_id, sqlite_i64(generation, "generation")?],
806 |row| {
807 let generation: i64 = row.get("generation")?;
808 let supervisor_pid: Option<i64> = row.get("supervisor_pid")?;
809 let last_heartbeat: Option<i64> = row.get("last_heartbeat")?;
810 Ok(SupervisorLeaseStatus {
811 generation: sqlite_u64(generation, "supervisor lease generation")
812 .map_err(|_| rusqlite::Error::InvalidQuery)?,
813 supervisor_pid: supervisor_pid.and_then(|pid| u32::try_from(pid).ok()),
814 supervisor_socket: row.get("supervisor_socket")?,
815 last_heartbeat: last_heartbeat
816 .map(|value| sqlite_u64(value, "supervisor last heartbeat"))
817 .transpose()
818 .map_err(|_| rusqlite::Error::InvalidQuery)?,
819 runtime_state: row.get("runtime_state")?,
820 })
821 },
822 )
823 .optional()
824 .context("failed to load supervisor lease from controller state")
825}
826
827pub fn load_dispatch_attempts_from_db(
828 conn: &Connection,
829 document_id: &str,
830) -> Result<Vec<DispatchAttemptStatus>> {
831 let mut stmt = conn.prepare(
832 r#"
833 SELECT
834 id,
835 generation,
836 command_kind,
837 accepted_stage,
838 failed_stage,
839 diagnostic_payload,
840 result_status,
841 proof_scope,
842 dispatch_start_proven,
843 timestamp
844 FROM dispatch_attempts
845 WHERE document_id = ?1
846 ORDER BY id DESC
847 LIMIT 10
848 "#,
849 )?;
850 let mut attempts = Vec::new();
851 for row in stmt.query_map(params![document_id], |row| {
852 let receipt_id: i64 = row.get("id")?;
853 let generation: i64 = row.get("generation")?;
854 let dispatch_start_proven: i64 = row.get("dispatch_start_proven")?;
855 let timestamp: i64 = row.get("timestamp")?;
856 Ok(DispatchAttemptStatus {
857 receipt_id: sqlite_u64(receipt_id, "dispatch receipt id")
858 .map_err(|_| rusqlite::Error::InvalidQuery)?,
859 generation: sqlite_u64(generation, "dispatch generation")
860 .map_err(|_| rusqlite::Error::InvalidQuery)?,
861 command_kind: row.get("command_kind")?,
862 accepted_stage: row.get("accepted_stage")?,
863 failed_stage: row.get("failed_stage")?,
864 diagnostic_payload: row.get("diagnostic_payload")?,
865 result_status: row.get("result_status")?,
866 proof_scope: row.get("proof_scope")?,
867 dispatch_start_proven: dispatch_start_proven != 0,
868 timestamp: sqlite_u64(timestamp, "dispatch timestamp")
869 .map_err(|_| rusqlite::Error::InvalidQuery)?,
870 })
871 })? {
872 attempts.push(row?);
873 }
874 attempts.reverse();
875 Ok(attempts)
876}
877
878pub fn load_projection_diagnostics_from_db(
879 conn: &Connection,
880 document_id: &str,
881) -> Result<Vec<ProjectionDiagnosticStatus>> {
882 let mut stmt = conn.prepare(
883 r#"
884 SELECT projection, message, source_generation, intended_hash, retry_status, timestamp
885 FROM projection_diagnostics
886 WHERE document_id = ?1
887 ORDER BY id DESC
888 LIMIT 10
889 "#,
890 )?;
891 let mut diagnostics = Vec::new();
892 for row in stmt.query_map(params![document_id], |row| {
893 let timestamp: i64 = row.get("timestamp")?;
894 let source_generation: Option<i64> = row.get("source_generation")?;
895 Ok(ProjectionDiagnosticStatus {
896 projection: row.get("projection")?,
897 message: row.get("message")?,
898 source_generation: source_generation
899 .map(|generation| sqlite_u64(generation, "projection source generation"))
900 .transpose()
901 .map_err(|_| rusqlite::Error::InvalidQuery)?,
902 intended_hash: row.get("intended_hash")?,
903 retry_status: row.get("retry_status")?,
904 timestamp: sqlite_u64(timestamp, "projection diagnostic timestamp")
905 .map_err(|_| rusqlite::Error::InvalidQuery)?,
906 })
907 })? {
908 diagnostics.push(row?);
909 }
910 diagnostics.reverse();
911 Ok(diagnostics)
912}
913
914pub fn load_queue_head_from_db(
915 conn: &Connection,
916 document_id: &str,
917 queue_name: &str,
918) -> Result<Option<QueueHeadStatus>> {
919 conn.query_row(
920 r#"
921 SELECT
922 document_id,
923 queue_name,
924 generation,
925 head_id,
926 prompt,
927 state,
928 priority,
929 selected_at,
930 updated_at
931 FROM queue_heads
932 WHERE document_id = ?1 AND queue_name = ?2
933 "#,
934 params![document_id, queue_name],
935 |row| {
936 let generation: Option<i64> = row.get("generation")?;
937 let priority: i64 = row.get("priority")?;
938 let selected_at: i64 = row.get("selected_at")?;
939 let updated_at: i64 = row.get("updated_at")?;
940 Ok(QueueHeadStatus {
941 document_id: row.get("document_id")?,
942 queue_name: row.get("queue_name")?,
943 generation: generation
944 .map(|generation| sqlite_u64(generation, "queue head generation"))
945 .transpose()
946 .map_err(|_| rusqlite::Error::InvalidQuery)?,
947 head_id: row.get("head_id")?,
948 prompt: row.get("prompt")?,
949 state: row.get("state")?,
950 priority: sqlite_u64(priority, "queue head priority")
951 .map_err(|_| rusqlite::Error::InvalidQuery)?,
952 selected_at: sqlite_u64(selected_at, "queue head selected_at")
953 .map_err(|_| rusqlite::Error::InvalidQuery)?,
954 updated_at: sqlite_u64(updated_at, "queue head updated_at")
955 .map_err(|_| rusqlite::Error::InvalidQuery)?,
956 })
957 },
958 )
959 .optional()
960 .context("failed to load queue head from controller state")
961}
962
963pub fn load_queue_control_from_db(
964 conn: &Connection,
965 scope_kind: &str,
966 scope_id: &str,
967) -> Result<Option<QueueControlStatus>> {
968 conn.query_row(
969 r#"
970 SELECT
971 id,
972 scope_kind,
973 scope_id,
974 state,
975 reason,
976 operation_receipt_id,
977 updated_at
978 FROM queue_controls
979 WHERE scope_kind = ?1 AND scope_id = ?2
980 "#,
981 params![scope_kind, scope_id],
982 |row| {
983 let receipt_id: i64 = row.get("id")?;
984 let operation_receipt_id: Option<i64> = row.get("operation_receipt_id")?;
985 let updated_at: i64 = row.get("updated_at")?;
986 Ok(QueueControlStatus {
987 receipt_id: sqlite_u64(receipt_id, "queue control receipt id")
988 .map_err(|_| rusqlite::Error::InvalidQuery)?,
989 scope_kind: row.get("scope_kind")?,
990 scope_id: row.get("scope_id")?,
991 state: row.get("state")?,
992 reason: row.get("reason")?,
993 operation_receipt_id: operation_receipt_id
994 .map(|value| sqlite_u64(value, "queue control operation receipt id"))
995 .transpose()
996 .map_err(|_| rusqlite::Error::InvalidQuery)?,
997 updated_at: sqlite_u64(updated_at, "queue control updated_at")
998 .map_err(|_| rusqlite::Error::InvalidQuery)?,
999 })
1000 },
1001 )
1002 .optional()
1003 .context("failed to load queue control from controller state")
1004}
1005
1006pub fn load_effective_queue_control_from_db(
1007 conn: &Connection,
1008 document_id: &str,
1009 project_scope_id: &str,
1010) -> Result<Option<QueueControlStatus>> {
1011 if let Some(control) = load_queue_control_from_db(conn, "document", document_id)?
1012 && control.state != "resumed"
1013 {
1014 return Ok(Some(control));
1015 }
1016 let project = load_queue_control_from_db(conn, "project", project_scope_id)?;
1017 Ok(project.filter(|control| control.state != "resumed"))
1018}
1019
1020pub fn load_admin_operations_from_db(
1021 conn: &Connection,
1022 document_id: Option<&str>,
1023 limit: usize,
1024) -> Result<Vec<AdminOperationStatus>> {
1025 let limit = i64::try_from(limit.max(1)).context("admin operation limit too large")?;
1026 let mut operations = Vec::new();
1027 if let Some(document_id) = document_id {
1028 let mut stmt = conn.prepare(
1029 r#"
1030 SELECT id, operation_kind, document_id, status, diagnostic_payload, timestamp
1031 FROM admin_operations
1032 WHERE document_id = ?1
1033 ORDER BY id DESC
1034 LIMIT ?2
1035 "#,
1036 )?;
1037 for row in stmt.query_map(params![document_id, limit], admin_operation_from_row)? {
1038 operations.push(row?);
1039 }
1040 } else {
1041 let mut stmt = conn.prepare(
1042 r#"
1043 SELECT id, operation_kind, document_id, status, diagnostic_payload, timestamp
1044 FROM admin_operations
1045 ORDER BY id DESC
1046 LIMIT ?1
1047 "#,
1048 )?;
1049 for row in stmt.query_map(params![limit], admin_operation_from_row)? {
1050 operations.push(row?);
1051 }
1052 }
1053 operations.reverse();
1054 Ok(operations)
1055}
1056
1057pub fn load_queue_backpressure_from_db(
1058 conn: &Connection,
1059 document_id: &str,
1060 limit: usize,
1061) -> Result<Vec<QueueBackpressureStatus>> {
1062 let limit = i64::try_from(limit.max(1)).context("queue backpressure limit too large")?;
1063 let mut stmt = conn.prepare(
1064 r#"
1065 SELECT
1066 id,
1067 document_id,
1068 generation,
1069 command_kind,
1070 capacity_class,
1071 reason,
1072 dispatch_receipt_id,
1073 timestamp
1074 FROM queue_backpressure
1075 WHERE document_id = ?1
1076 ORDER BY id DESC
1077 LIMIT ?2
1078 "#,
1079 )?;
1080 let mut receipts = Vec::new();
1081 for row in stmt.query_map(params![document_id, limit], |row| {
1082 let receipt_id: i64 = row.get("id")?;
1083 let generation: Option<i64> = row.get("generation")?;
1084 let dispatch_receipt_id: Option<i64> = row.get("dispatch_receipt_id")?;
1085 let timestamp: i64 = row.get("timestamp")?;
1086 Ok(QueueBackpressureStatus {
1087 receipt_id: sqlite_u64(receipt_id, "queue backpressure receipt id")
1088 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1089 document_id: row.get("document_id")?,
1090 generation: generation
1091 .map(|generation| sqlite_u64(generation, "queue backpressure generation"))
1092 .transpose()
1093 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1094 command_kind: row.get("command_kind")?,
1095 capacity_class: row.get("capacity_class")?,
1096 reason: row.get("reason")?,
1097 dispatch_receipt_id: dispatch_receipt_id
1098 .map(|receipt_id| sqlite_u64(receipt_id, "queue backpressure dispatch receipt id"))
1099 .transpose()
1100 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1101 timestamp: sqlite_u64(timestamp, "queue backpressure timestamp")
1102 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1103 })
1104 })? {
1105 receipts.push(row?);
1106 }
1107 receipts.reverse();
1108 Ok(receipts)
1109}
1110
1111fn admin_operation_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<AdminOperationStatus> {
1112 let receipt_id: i64 = row.get("id")?;
1113 let timestamp: i64 = row.get("timestamp")?;
1114 Ok(AdminOperationStatus {
1115 receipt_id: sqlite_u64(receipt_id, "admin operation receipt id")
1116 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1117 operation_kind: row.get("operation_kind")?,
1118 document_id: row.get("document_id")?,
1119 status: row.get("status")?,
1120 diagnostic_payload: row.get("diagnostic_payload")?,
1121 timestamp: sqlite_u64(timestamp, "admin operation timestamp")
1122 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1123 })
1124}
1125
1126pub fn load_session_operator_status_from_db(
1127 conn: &Connection,
1128 document_id: &str,
1129) -> Result<SessionOperatorStatus> {
1130 let record = load_actor_record_from_db(conn, document_id)?;
1131 let supervisor_lease = match &record {
1132 Some(record) => load_supervisor_lease_from_db(conn, document_id, record.generation)?,
1133 None => None,
1134 };
1135 Ok(SessionOperatorStatus {
1136 record,
1137 transitions: load_actor_transitions_from_db(conn, document_id)?,
1138 supervisor_lease,
1139 dispatch_attempts: load_dispatch_attempts_from_db(conn, document_id)?,
1140 projection_diagnostics: load_projection_diagnostics_from_db(conn, document_id)?,
1141 })
1142}
1143
1144fn state_event_status_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<StateEventStatus> {
1149 let sequence: i64 = row.get("id")?;
1150 let timestamp: i64 = row.get("timestamp")?;
1151 Ok(StateEventStatus {
1152 sequence: sqlite_u64(sequence, "state event sequence")
1153 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1154 event_id: row.get("event_id")?,
1155 document_hash: row.get("document_hash")?,
1156 domain: row.get("domain")?,
1157 fact_type: row.get("fact_type")?,
1158 payload_json: row.get("payload_json")?,
1159 timestamp: sqlite_u64(timestamp, "state event timestamp")
1160 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1161 })
1162}
1163
1164pub fn load_state_events_from_db(
1165 conn: &Connection,
1166 document_hash: Option<&str>,
1167) -> Result<Vec<StateEventStatus>> {
1168 let mut events = Vec::new();
1169 if let Some(document_hash) = document_hash {
1170 let mut stmt = conn.prepare(
1171 r#"
1172 SELECT id, event_id, document_hash, domain, fact_type, payload_json, timestamp
1173 FROM state_events
1174 WHERE document_hash = ?1
1175 ORDER BY id
1176 "#,
1177 )?;
1178 for row in stmt.query_map(params![document_hash], state_event_status_from_row)? {
1179 events.push(row?);
1180 }
1181 } else {
1182 let mut stmt = conn.prepare(
1183 r#"
1184 SELECT id, event_id, document_hash, domain, fact_type, payload_json, timestamp
1185 FROM state_events
1186 ORDER BY id
1187 "#,
1188 )?;
1189 for row in stmt.query_map([], state_event_status_from_row)? {
1190 events.push(row?);
1191 }
1192 }
1193 Ok(events)
1194}
1195
1196pub fn insert_actor_transition(
1201 conn: &Connection,
1202 previous: Option<&ActorRecord>,
1203 record: &ActorRecord,
1204) -> Result<i64> {
1205 conn.execute(
1206 r#"
1207 INSERT INTO actor_transitions (
1208 document_id,
1209 prior_generation,
1210 new_generation,
1211 caller,
1212 reason,
1213 old_pane,
1214 new_pane,
1215 old_window,
1216 new_window,
1217 timestamp
1218 )
1219 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
1220 "#,
1221 params![
1222 record.document_id,
1223 sqlite_i64(record.last_transition.prior_generation, "prior_generation")?,
1224 sqlite_i64(record.last_transition.new_generation, "new_generation")?,
1225 record.last_transition.caller,
1226 record.last_transition.reason,
1227 previous.map(|prior| prior.pane_id.as_str()),
1228 record.pane_id,
1229 previous.map(|prior| prior.window_id.as_str()),
1230 record.window_id,
1231 sqlite_i64(record.last_transition.timestamp, "transition timestamp")?,
1232 ],
1233 )?;
1234 Ok(conn.last_insert_rowid())
1235}
1236
1237pub fn upsert_actor_document(
1244 conn: &Connection,
1245 record: &ActorRecord,
1246 transition_id: i64,
1247 launch_mode: Option<String>,
1248 controller_epoch: Option<i64>,
1249) -> Result<()> {
1250 conn.execute(
1251 r#"
1252 INSERT INTO documents (
1253 document_id,
1254 canonical_path,
1255 session_id,
1256 generation,
1257 pane_id,
1258 window_id,
1259 harness_id,
1260 actor_state,
1261 launch_mode,
1262 controller_epoch,
1263 last_transition_id
1264 )
1265 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
1266 ON CONFLICT(document_id) DO UPDATE SET
1267 canonical_path = excluded.canonical_path,
1268 session_id = excluded.session_id,
1269 generation = excluded.generation,
1270 pane_id = excluded.pane_id,
1271 window_id = excluded.window_id,
1272 harness_id = excluded.harness_id,
1273 actor_state = excluded.actor_state,
1274 launch_mode = excluded.launch_mode,
1275 controller_epoch = excluded.controller_epoch,
1276 last_transition_id = excluded.last_transition_id
1277 "#,
1278 params![
1279 record.document_id,
1280 record.document_id,
1281 record.session_id,
1282 sqlite_i64(record.generation, "generation")?,
1283 record.pane_id,
1284 record.window_id,
1285 record.harness,
1286 record.state.as_str(),
1287 launch_mode,
1288 controller_epoch,
1289 transition_id,
1290 ],
1291 )?;
1292 Ok(())
1293}
1294
1295pub fn upsert_supervisor_lease_in_db(
1296 conn: &Connection,
1297 record: &ActorRecord,
1298 supervisor_pid: Option<u32>,
1299 supervisor_socket: Option<&str>,
1300 runtime_state: &str,
1301) -> Result<()> {
1302 conn.execute(
1303 r#"
1304 INSERT INTO supervisor_leases (
1305 document_id,
1306 generation,
1307 supervisor_pid,
1308 supervisor_socket,
1309 last_heartbeat,
1310 runtime_state
1311 )
1312 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1313 ON CONFLICT(document_id, generation) DO UPDATE SET
1314 supervisor_pid = COALESCE(excluded.supervisor_pid, supervisor_leases.supervisor_pid),
1315 supervisor_socket = COALESCE(excluded.supervisor_socket, supervisor_leases.supervisor_socket),
1316 last_heartbeat = excluded.last_heartbeat,
1317 runtime_state = excluded.runtime_state
1318 "#,
1319 params![
1320 record.document_id,
1321 sqlite_i64(record.generation, "generation")?,
1322 supervisor_pid.map(i64::from),
1323 supervisor_socket,
1324 sqlite_i64(timestamp_secs(), "supervisor heartbeat timestamp")?,
1325 runtime_state,
1326 ],
1327 )?;
1328 Ok(())
1329}
1330
1331pub fn insert_state_event_in_db(conn: &Connection, event: &StateEventInsert<'_>) -> Result<bool> {
1332 let changed = conn.execute(
1333 r#"
1334 INSERT OR IGNORE INTO state_events (
1335 event_id,
1336 document_hash,
1337 domain,
1338 fact_type,
1339 payload_json,
1340 timestamp
1341 )
1342 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1343 "#,
1344 params![
1345 event.event_id,
1346 event.document_hash,
1347 event.domain,
1348 event.fact_type,
1349 event.payload_json,
1350 sqlite_i64(timestamp_secs(), "state event timestamp")?,
1351 ],
1352 )?;
1353 Ok(changed > 0)
1354}
1355
1356pub fn has_open_in_flight_dispatch(
1361 conn: &Connection,
1362 document_id: &str,
1363 generation: u64,
1364) -> Result<bool> {
1365 let count: i64 = conn.query_row(
1366 r#"
1367 SELECT COUNT(*)
1368 FROM dispatch_attempts
1369 WHERE document_id = ?1
1370 AND generation = ?2
1371 AND failed_stage IS NULL
1372 AND COALESCE(result_status, '') IN ('accepted', 'queued', 'running')
1373 AND dispatch_start_proven = 0
1374 "#,
1375 params![document_id, generation as i64],
1376 |row| row.get(0),
1377 )?;
1378 Ok(count > 0)
1379}
1380
1381pub fn has_any_open_in_flight_dispatch(conn: &Connection) -> Result<bool> {
1387 let count: i64 = conn.query_row(
1388 r#"
1389 SELECT COUNT(*)
1390 FROM dispatch_attempts
1391 WHERE failed_stage IS NULL
1392 AND COALESCE(result_status, '') IN ('accepted', 'queued', 'running')
1393 AND dispatch_start_proven = 0
1394 "#,
1395 [],
1396 |row| row.get(0),
1397 )?;
1398 Ok(count > 0)
1399}
1400
1401pub fn mark_open_dispatches_consumed(conn: &Connection, document_id: &str) -> Result<usize> {
1406 let released = conn.execute(
1407 r#"
1408 UPDATE dispatch_attempts
1409 SET dispatch_start_proven = 1
1410 WHERE document_id = ?1
1411 AND failed_stage IS NULL
1412 AND COALESCE(result_status, '') IN ('accepted', 'queued', 'running')
1413 AND dispatch_start_proven = 0
1414 "#,
1415 params![document_id],
1416 )?;
1417 Ok(released)
1418}
1419
1420pub fn insert_dispatch_attempt_in_db(
1421 conn: &Connection,
1422 attempt: &DispatchAttemptInsert<'_>,
1423) -> Result<u64> {
1424 conn.execute(
1425 r#"
1426 INSERT INTO dispatch_attempts (
1427 document_id,
1428 generation,
1429 command_kind,
1430 accepted_stage,
1431 failed_stage,
1432 diagnostic_payload,
1433 result_status,
1434 proof_scope,
1435 dispatch_start_proven,
1436 timestamp
1437 )
1438 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
1439 "#,
1440 params![
1441 attempt.document_id,
1442 sqlite_i64(attempt.generation, "generation")?,
1443 attempt.command_kind,
1444 attempt.accepted_stage,
1445 attempt.failed_stage,
1446 attempt.diagnostic_payload,
1447 attempt.result_status,
1448 attempt.proof_scope,
1449 if attempt.dispatch_start_proven {
1450 1_i64
1451 } else {
1452 0_i64
1453 },
1454 sqlite_i64(timestamp_secs(), "dispatch attempt timestamp")?,
1455 ],
1456 )?;
1457 sqlite_u64(conn.last_insert_rowid(), "dispatch receipt id")
1458}
1459
1460pub fn insert_projection_diagnostic(
1461 conn: &Connection,
1462 projection: &str,
1463 document_id: &str,
1464 message: &str,
1465) -> Result<()> {
1466 insert_projection_diagnostic_with_metadata(
1467 conn,
1468 &ProjectionDiagnosticInsert {
1469 projection,
1470 document_id,
1471 message,
1472 source_generation: None,
1473 intended_hash: None,
1474 retry_status: "retry_pending",
1475 },
1476 )
1477}
1478
1479pub fn insert_projection_diagnostic_with_metadata(
1480 conn: &Connection,
1481 diagnostic: &ProjectionDiagnosticInsert<'_>,
1482) -> Result<()> {
1483 conn.execute(
1484 r#"
1485 INSERT INTO projection_diagnostics (
1486 projection,
1487 document_id,
1488 message,
1489 source_generation,
1490 intended_hash,
1491 retry_status,
1492 timestamp
1493 )
1494 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
1495 "#,
1496 params![
1497 diagnostic.projection,
1498 diagnostic.document_id,
1499 diagnostic.message,
1500 diagnostic
1501 .source_generation
1502 .map(|generation| sqlite_i64(generation, "projection source generation"))
1503 .transpose()?,
1504 diagnostic.intended_hash,
1505 diagnostic.retry_status,
1506 sqlite_i64(timestamp_secs(), "projection diagnostic timestamp")?
1507 ],
1508 )?;
1509 Ok(())
1510}
1511
1512fn evict_cross_document_actor_pane_bindings_tx(
1513 conn: &Connection,
1514 owner_document_id: &str,
1515 pane_id: &str,
1516 caller: &str,
1517 timestamp: u64,
1518 launch_mode: Option<String>,
1519 controller_epoch: Option<i64>,
1520) -> Result<Vec<String>> {
1521 if pane_id.is_empty() {
1522 return Ok(Vec::new());
1523 }
1524 let store = load_actor_store_from_db(conn)?;
1525 let mut evicted = Vec::new();
1526 for prior in store.values() {
1527 if prior.document_id == owner_document_id || prior.pane_id != pane_id {
1528 continue;
1529 }
1530 let mut next = prior.clone();
1531 next.state = ActorState::Closed;
1532 next.pane_id.clear();
1533 next.window_id.clear();
1534 next.last_transition = ActorLastTransition {
1535 caller: caller.to_string(),
1536 reason: format!("evicted_cross_document_pane owner={owner_document_id} pane={pane_id}"),
1537 timestamp,
1538 prior_generation: prior.generation,
1539 new_generation: prior.generation,
1540 };
1541 let transition_id = insert_actor_transition(conn, Some(prior), &next)?;
1542 upsert_actor_document(
1543 conn,
1544 &next,
1545 transition_id,
1546 launch_mode.clone(),
1547 controller_epoch,
1548 )?;
1549 evicted.push(prior.document_id.clone());
1550 }
1551 Ok(evicted)
1552}
1553
1554pub fn store_actor_record_tx(
1562 conn: &mut Connection,
1563 expected_prior_generation: Option<u64>,
1564 record: &ActorRecord,
1565 launch_mode: Option<String>,
1566 controller_epoch: Option<i64>,
1567) -> Result<Vec<String>> {
1568 let tx = conn.transaction()?;
1569 let previous = load_actor_record_from_db(&tx, &record.document_id)?;
1570 let prior_generation = previous
1571 .as_ref()
1572 .map(|prior| prior.generation)
1573 .unwrap_or(record.last_transition.prior_generation);
1574 if let Some(expected) = expected_prior_generation
1575 && prior_generation != expected
1576 {
1577 anyhow::bail!(
1578 "controller actor generation compare-and-swap failed for {}: expected {}, found {}",
1579 record.document_id,
1580 expected,
1581 prior_generation
1582 );
1583 }
1584 if record.generation < prior_generation {
1585 anyhow::bail!(
1586 "controller actor generation regression for {}: attempted {}, current {}",
1587 record.document_id,
1588 record.generation,
1589 prior_generation
1590 );
1591 }
1592 let evicted_document_ids = if record.state != ActorState::Closed {
1593 evict_cross_document_actor_pane_bindings_tx(
1594 &tx,
1595 &record.document_id,
1596 &record.pane_id,
1597 &record.last_transition.caller,
1598 record.last_transition.timestamp,
1599 launch_mode.clone(),
1600 controller_epoch,
1601 )?
1602 } else {
1603 Vec::new()
1604 };
1605 let transition_id = insert_actor_transition(&tx, previous.as_ref(), record)?;
1606 upsert_actor_document(&tx, record, transition_id, launch_mode, controller_epoch)?;
1607 tx.commit()?;
1608 Ok(evicted_document_ids)
1609}
1610
1611pub fn delete_actor_document_tx(conn: &mut Connection, document_id: &str) -> Result<usize> {
1620 let tx = conn.transaction()?;
1621 tx.execute(
1622 "DELETE FROM actor_transitions WHERE document_id = ?1",
1623 params![document_id],
1624 )?;
1625 tx.execute(
1626 "DELETE FROM supervisor_leases WHERE document_id = ?1",
1627 params![document_id],
1628 )?;
1629 let removed = tx.execute(
1630 "DELETE FROM documents WHERE document_id = ?1",
1631 params![document_id],
1632 )?;
1633 tx.commit()?;
1634 Ok(removed)
1635}
1636
1637pub fn actor_documents_empty(conn: &Connection) -> Result<bool> {
1642 let count: i64 = conn.query_row("SELECT COUNT(*) FROM documents", [], |row| row.get(0))?;
1643 Ok(count == 0)
1644}
1645
1646pub fn migrate_actor_store_tx(
1652 conn: &mut Connection,
1653 store: &BTreeMap<String, ActorRecord>,
1654 launch_mode: Option<String>,
1655 controller_epoch: Option<i64>,
1656) -> Result<()> {
1657 let count: i64 = conn.query_row("SELECT COUNT(*) FROM documents", [], |row| row.get(0))?;
1658 if count > 0 {
1659 return Ok(());
1660 }
1661 let tx = conn.transaction()?;
1662 for record in store.values() {
1663 let transition_id = insert_actor_transition(&tx, None, record)?;
1664 upsert_actor_document(
1665 &tx,
1666 record,
1667 transition_id,
1668 launch_mode.clone(),
1669 controller_epoch,
1670 )?;
1671 }
1672 tx.commit()?;
1673 Ok(())
1674}
1675
1676pub fn upsert_queue_head_in_db(
1681 conn: &Connection,
1682 document_id: &str,
1683 queue_name: &str,
1684 head_id: Option<&str>,
1685 prompt: &str,
1686 state: &str,
1687) -> Result<()> {
1688 let now = sqlite_i64(timestamp_secs(), "queue head timestamp")?;
1689 conn.execute(
1690 r#"
1691 INSERT INTO queue_heads (
1692 document_id,
1693 queue_name,
1694 head_id,
1695 prompt,
1696 state,
1697 selected_at,
1698 updated_at
1699 )
1700 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6)
1701 ON CONFLICT(document_id, queue_name) DO UPDATE SET
1702 head_id = excluded.head_id,
1703 prompt = excluded.prompt,
1704 state = excluded.state,
1705 updated_at = excluded.updated_at
1706 "#,
1707 params![document_id, queue_name, head_id, prompt, state, now],
1708 )?;
1709 Ok(())
1710}
1711
1712pub fn upsert_queue_control_in_db(
1713 conn: &Connection,
1714 control: &QueueControlInsert<'_>,
1715) -> Result<QueueControlStatus> {
1716 let now = sqlite_i64(timestamp_secs(), "queue control timestamp")?;
1717 conn.execute(
1718 r#"
1719 INSERT INTO queue_controls (
1720 scope_kind,
1721 scope_id,
1722 state,
1723 reason,
1724 operation_receipt_id,
1725 updated_at
1726 )
1727 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1728 ON CONFLICT(scope_kind, scope_id) DO UPDATE SET
1729 state = excluded.state,
1730 reason = excluded.reason,
1731 operation_receipt_id = excluded.operation_receipt_id,
1732 updated_at = excluded.updated_at
1733 "#,
1734 params![
1735 control.scope_kind,
1736 control.scope_id,
1737 control.state,
1738 control.reason,
1739 control
1740 .operation_receipt_id
1741 .map(|receipt_id| sqlite_i64(receipt_id, "queue control operation receipt id"))
1742 .transpose()?,
1743 now
1744 ],
1745 )?;
1746 load_queue_control_from_db(conn, control.scope_kind, control.scope_id)?
1747 .context("missing queue control after upsert")
1748}
1749
1750pub fn insert_queue_backpressure_in_db(
1751 conn: &Connection,
1752 backpressure: &QueueBackpressureInsert<'_>,
1753) -> Result<QueueBackpressureStatus> {
1754 conn.execute(
1755 r#"
1756 INSERT INTO queue_backpressure (
1757 document_id,
1758 generation,
1759 command_kind,
1760 capacity_class,
1761 reason,
1762 dispatch_receipt_id,
1763 timestamp
1764 )
1765 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
1766 "#,
1767 params![
1768 backpressure.document_id,
1769 backpressure
1770 .generation
1771 .map(|generation| sqlite_i64(generation, "queue backpressure generation"))
1772 .transpose()?,
1773 backpressure.command_kind,
1774 backpressure.capacity_class,
1775 backpressure.reason,
1776 backpressure
1777 .dispatch_receipt_id
1778 .map(|receipt_id| sqlite_i64(receipt_id, "queue backpressure dispatch receipt id"))
1779 .transpose()?,
1780 sqlite_i64(timestamp_secs(), "queue backpressure timestamp")?
1781 ],
1782 )?;
1783 let receipt_id = sqlite_u64(conn.last_insert_rowid(), "queue backpressure receipt id")?;
1784 Ok(QueueBackpressureStatus {
1785 receipt_id,
1786 document_id: backpressure.document_id.to_string(),
1787 generation: backpressure.generation,
1788 command_kind: backpressure.command_kind.to_string(),
1789 capacity_class: backpressure.capacity_class.to_string(),
1790 reason: backpressure.reason.to_string(),
1791 dispatch_receipt_id: backpressure.dispatch_receipt_id,
1792 timestamp: timestamp_secs(),
1793 })
1794}
1795
1796pub fn upsert_document_cycle_state_in_db(
1797 conn: &Connection,
1798 document_id: &str,
1799 cycle_id: &str,
1800 state: &str,
1801 queue_head_id: Option<&str>,
1802 response_commit: Option<&str>,
1803) -> Result<()> {
1804 conn.execute(
1805 r#"
1806 INSERT INTO document_cycles (
1807 document_id,
1808 cycle_id,
1809 state,
1810 queue_head_id,
1811 response_commit,
1812 updated_at
1813 )
1814 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1815 ON CONFLICT(document_id, cycle_id) DO UPDATE SET
1816 state = excluded.state,
1817 queue_head_id = excluded.queue_head_id,
1818 response_commit = excluded.response_commit,
1819 updated_at = excluded.updated_at
1820 "#,
1821 params![
1822 document_id,
1823 cycle_id,
1824 state,
1825 queue_head_id,
1826 response_commit,
1827 sqlite_i64(timestamp_secs(), "document cycle timestamp")?
1828 ],
1829 )?;
1830 Ok(())
1831}
1832
1833pub fn commit_session_actor_closeout_in_db(
1834 conn: &mut Connection,
1835 closeout: &SessionActorCloseoutCommit<'_>,
1836) -> Result<()> {
1837 let now = sqlite_i64(timestamp_secs(), "session actor closeout timestamp")?;
1838 let tx = conn.transaction()?;
1839
1840 if let Some(prompt) = closeout.queue_head_prompt {
1841 tx.execute(
1842 r#"
1843 INSERT INTO queue_heads (
1844 document_id,
1845 queue_name,
1846 head_id,
1847 prompt,
1848 state,
1849 selected_at,
1850 updated_at
1851 )
1852 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6)
1853 ON CONFLICT(document_id, queue_name) DO UPDATE SET
1854 head_id = excluded.head_id,
1855 prompt = excluded.prompt,
1856 state = excluded.state,
1857 updated_at = excluded.updated_at
1858 "#,
1859 params![
1860 closeout.document_id,
1861 closeout.queue_name,
1862 closeout.queue_head_id,
1863 prompt,
1864 closeout.queue_head_state,
1865 now
1866 ],
1867 )?;
1868 }
1869
1870 tx.execute(
1871 r#"
1872 INSERT INTO document_cycles (
1873 document_id,
1874 cycle_id,
1875 state,
1876 queue_head_id,
1877 response_commit,
1878 updated_at
1879 )
1880 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1881 ON CONFLICT(document_id, cycle_id) DO UPDATE SET
1882 state = excluded.state,
1883 queue_head_id = excluded.queue_head_id,
1884 response_commit = excluded.response_commit,
1885 updated_at = excluded.updated_at
1886 "#,
1887 params![
1888 closeout.document_id,
1889 closeout.cycle_id,
1890 closeout.cycle_state,
1891 closeout.queue_head_id,
1892 closeout.response_commit,
1893 now
1894 ],
1895 )?;
1896
1897 for mutation in &closeout.mutations {
1898 tx.execute(
1899 r#"
1900 INSERT INTO pending_mutations (
1901 document_id,
1902 cycle_id,
1903 item_id,
1904 mutation_kind,
1905 status,
1906 recorded_at
1907 )
1908 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1909 ON CONFLICT(document_id, cycle_id, item_id, mutation_kind) DO UPDATE SET
1910 status = excluded.status,
1911 recorded_at = excluded.recorded_at
1912 "#,
1913 params![
1914 closeout.document_id,
1915 closeout.cycle_id,
1916 mutation.item_id,
1917 mutation.mutation_kind,
1918 mutation.status,
1919 now
1920 ],
1921 )?;
1922 }
1923
1924 tx.commit()?;
1925 Ok(())
1926}
1927
1928pub fn insert_admin_operation_in_db(
1929 conn: &Connection,
1930 operation_kind: &str,
1931 document_id: Option<&str>,
1932 status: &str,
1933 diagnostic_payload: Option<&str>,
1934) -> Result<u64> {
1935 conn.execute(
1936 r#"
1937 INSERT INTO admin_operations (
1938 operation_kind,
1939 document_id,
1940 status,
1941 diagnostic_payload,
1942 timestamp
1943 )
1944 VALUES (?1, ?2, ?3, ?4, ?5)
1945 "#,
1946 params![
1947 operation_kind,
1948 document_id,
1949 status,
1950 diagnostic_payload,
1951 sqlite_i64(timestamp_secs(), "admin operation timestamp")?
1952 ],
1953 )?;
1954 sqlite_u64(conn.last_insert_rowid(), "admin operation receipt id")
1955}
1956
1957pub fn insert_crash_recovery_marker_in_db(
1958 conn: &Connection,
1959 marker_kind: &str,
1960 document_id: Option<&str>,
1961 generation: Option<u64>,
1962 status: &str,
1963 diagnostic_payload: Option<&str>,
1964) -> Result<()> {
1965 conn.execute(
1966 r#"
1967 INSERT INTO crash_recovery_markers (
1968 marker_kind,
1969 document_id,
1970 generation,
1971 status,
1972 diagnostic_payload,
1973 timestamp
1974 )
1975 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1976 "#,
1977 params![
1978 marker_kind,
1979 document_id,
1980 generation
1981 .map(|value| sqlite_i64(value, "crash recovery marker generation"))
1982 .transpose()?,
1983 status,
1984 diagnostic_payload,
1985 sqlite_i64(timestamp_secs(), "crash recovery marker timestamp")?
1986 ],
1987 )?;
1988 Ok(())
1989}
1990
1991pub fn load_layout_state_from_db(conn: &Connection, scope: &str) -> Result<Vec<String>> {
1996 let raw: Option<String> = conn
1997 .query_row(
1998 "SELECT columns_json FROM layout_states WHERE scope = ?1",
1999 params![scope],
2000 |row| row.get(0),
2001 )
2002 .optional()?;
2003 match raw {
2004 Some(raw) => serde_json::from_str(&raw).context("failed to parse layout state from sqlite"),
2005 None => Ok(Vec::new()),
2006 }
2007}
2008
2009pub fn store_layout_state_in_db(conn: &Connection, scope: &str, columns: &[String]) -> Result<()> {
2010 let columns_json = serde_json::to_string(columns)?;
2011 conn.execute(
2012 r#"
2013 INSERT INTO layout_states (scope, columns_json, updated_at)
2014 VALUES (?1, ?2, ?3)
2015 ON CONFLICT(scope) DO UPDATE SET
2016 columns_json = excluded.columns_json,
2017 updated_at = excluded.updated_at
2018 "#,
2019 params![
2020 scope,
2021 columns_json,
2022 sqlite_i64(timestamp_secs(), "layout state timestamp")?
2023 ],
2024 )?;
2025 Ok(())
2026}
2027
2028pub fn layout_scope_exists(conn: &Connection, scope: &str) -> Result<bool> {
2029 let exists: Option<i64> = conn
2030 .query_row(
2031 "SELECT 1 FROM layout_states WHERE scope = ?1",
2032 params![scope],
2033 |row| row.get(0),
2034 )
2035 .optional()?;
2036 Ok(exists.is_some())
2037}
2038
2039#[cfg(test)]
2040mod tests {
2041 use super::*;
2042
2043 #[test]
2044 fn control_plane_store_counts_extended_fact_categories() -> Result<()> {
2045 let dir = tempfile::TempDir::new()?;
2046 let mut conn = open_state_db(dir.path())?;
2047 let record = ActorRecord {
2048 document_id: "tasks/control-plane.md".to_string(),
2049 session_id: "session-control-plane".to_string(),
2050 generation: 1,
2051 pane_id: "%1".to_string(),
2052 window_id: "@1".to_string(),
2053 harness: "codex".to_string(),
2054 state: ActorState::Ready,
2055 last_transition: ActorLastTransition {
2056 caller: "test".to_string(),
2057 reason: "store_actor_categories".to_string(),
2058 timestamp: timestamp_secs(),
2059 prior_generation: 0,
2060 new_generation: 1,
2061 },
2062 };
2063
2064 store_actor_record_tx(
2065 &mut conn,
2066 None,
2067 &record,
2068 Some("managed".to_string()),
2069 Some(7),
2070 )?;
2071 upsert_supervisor_lease_in_db(
2072 &conn,
2073 &record,
2074 Some(1234),
2075 Some("supervisor.sock"),
2076 "ready",
2077 )?;
2078 assert!(insert_state_event_in_db(
2079 &conn,
2080 &StateEventInsert {
2081 event_id: "state-event-1",
2082 document_hash: &record.document_id,
2083 domain: "document",
2084 fact_type: "file_watch_change_observed",
2085 payload_json: r#"{"event_id":"state-event-1"}"#,
2086 },
2087 )?);
2088 assert!(!insert_state_event_in_db(
2089 &conn,
2090 &StateEventInsert {
2091 event_id: "state-event-1",
2092 document_hash: &record.document_id,
2093 domain: "document",
2094 fact_type: "file_watch_change_observed",
2095 payload_json: r#"{"event_id":"state-event-1"}"#,
2096 },
2097 )?);
2098 insert_dispatch_attempt_in_db(
2099 &conn,
2100 &DispatchAttemptInsert {
2101 document_id: &record.document_id,
2102 generation: record.generation,
2103 command_kind: "managed_reopen",
2104 accepted_stage: Some("accepted"),
2105 failed_stage: None,
2106 diagnostic_payload: "store actor count test",
2107 result_status: "accepted",
2108 proof_scope: "accepted_only",
2109 dispatch_start_proven: false,
2110 },
2111 )?;
2112 upsert_queue_head_in_db(
2113 &conn,
2114 &record.document_id,
2115 "agent:queue",
2116 Some("ctrlplane-storeactor"),
2117 "do [#ctrlplane-storeactor]",
2118 "selected",
2119 )?;
2120 let queue_head =
2121 load_queue_head_from_db(&conn, &record.document_id, "agent:queue")?.unwrap();
2122 assert_eq!(queue_head.generation, None);
2123 assert_eq!(queue_head.priority, 0);
2124 upsert_document_cycle_state_in_db(
2125 &conn,
2126 &record.document_id,
2127 "cycle-storeactor",
2128 "preflight_started",
2129 Some("ctrlplane-storeactor"),
2130 None,
2131 )?;
2132 commit_session_actor_closeout_in_db(
2133 &mut conn,
2134 &SessionActorCloseoutCommit {
2135 document_id: &record.document_id,
2136 cycle_id: "cycle-storeactor",
2137 cycle_state: "committed",
2138 queue_name: "agent:queue",
2139 queue_head_id: Some("ctrlplane-storeactor"),
2140 queue_head_prompt: Some("do [#ctrlplane-storeactor]"),
2141 queue_head_state: "consumed",
2142 response_commit: Some("commit-storeactor"),
2143 mutations: vec![SessionActorCloseoutMutation {
2144 item_id: "ctrlplane-storeactor",
2145 mutation_kind: "backlog_completion",
2146 status: "done",
2147 }],
2148 },
2149 )?;
2150 insert_projection_diagnostic(
2151 &conn,
2152 "session-actors.json",
2153 &record.document_id,
2154 "projection lag",
2155 )?;
2156 insert_admin_operation_in_db(
2157 &conn,
2158 "projection_repair",
2159 Some(&record.document_id),
2160 "accepted",
2161 Some("store actor count test"),
2162 )?;
2163 let queue_control = upsert_queue_control_in_db(
2164 &conn,
2165 &QueueControlInsert {
2166 scope_kind: "document",
2167 scope_id: &record.document_id,
2168 state: "paused",
2169 reason: Some("store actor count test"),
2170 operation_receipt_id: Some(1),
2171 },
2172 )?;
2173 insert_queue_backpressure_in_db(
2174 &conn,
2175 &QueueBackpressureInsert {
2176 document_id: &record.document_id,
2177 generation: Some(record.generation),
2178 command_kind: "managed_reopen",
2179 capacity_class: "queue_paused",
2180 reason: "store actor count test",
2181 dispatch_receipt_id: Some(queue_control.receipt_id),
2182 },
2183 )?;
2184 insert_crash_recovery_marker_in_db(
2185 &conn,
2186 "startup_reconcile",
2187 Some(&record.document_id),
2188 Some(record.generation),
2189 "pending",
2190 Some("store actor count test"),
2191 )?;
2192 store_layout_state_in_db(&conn, "default", &["%1".to_string()])?;
2193
2194 let counts = load_control_plane_store_counts(&conn)?;
2195 assert_eq!(counts.actor_documents, 1);
2196 assert_eq!(counts.live_actor_documents, 1);
2197 assert_eq!(counts.actor_transitions, 1);
2198 assert_eq!(counts.supervisor_leases, 1);
2199 assert_eq!(counts.state_events, 1);
2200 assert_eq!(counts.dispatch_receipts, 1);
2201 assert_eq!(counts.queue_heads, 1);
2202 assert_eq!(counts.document_cycles, 1);
2203 assert_eq!(counts.pending_mutations, 1);
2204 assert_eq!(counts.projection_diagnostics, 1);
2205 assert_eq!(counts.admin_operations, 1);
2206 assert_eq!(counts.queue_controls, 1);
2207 assert_eq!(counts.queue_backpressure, 1);
2208 assert_eq!(counts.crash_recovery_markers, 1);
2209 assert_eq!(counts.layout_states, 1);
2210 assert_eq!(counts.total_authoritative_rows(), 14);
2211
2212 let state_events = load_state_events_from_db(&conn, Some(&record.document_id))?;
2213 assert_eq!(state_events.len(), 1);
2214 assert_eq!(state_events[0].event_id, "state-event-1");
2215 assert_eq!(state_events[0].domain, "document");
2216 assert_eq!(state_events[0].fact_type, "file_watch_change_observed");
2217
2218 Ok(())
2219 }
2220}