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 dispatch_receipts: usize,
240 pub queue_heads: usize,
241 pub document_cycles: usize,
242 pub pending_mutations: usize,
243 pub projection_diagnostics: usize,
244 pub admin_operations: usize,
245 pub queue_controls: usize,
246 pub queue_backpressure: usize,
247 pub crash_recovery_markers: usize,
248 pub layout_states: usize,
249}
250
251impl ControlPlaneStoreCounts {
252 pub fn total_authoritative_rows(&self) -> usize {
253 self.actor_documents
254 + self.actor_transitions
255 + self.supervisor_leases
256 + self.dispatch_receipts
257 + self.queue_heads
258 + self.document_cycles
259 + self.pending_mutations
260 + self.projection_diagnostics
261 + self.admin_operations
262 + self.queue_controls
263 + self.queue_backpressure
264 + self.crash_recovery_markers
265 + self.layout_states
266 }
267}
268
269#[derive(Clone, Debug, PartialEq, Eq)]
270pub struct SessionActorCloseoutMutation<'a> {
271 pub item_id: &'a str,
272 pub mutation_kind: &'a str,
273 pub status: &'a str,
274}
275
276#[derive(Clone, Debug, PartialEq, Eq)]
277pub struct SessionActorCloseoutCommit<'a> {
278 pub document_id: &'a str,
279 pub cycle_id: &'a str,
280 pub cycle_state: &'a str,
281 pub queue_name: &'a str,
282 pub queue_head_id: Option<&'a str>,
283 pub queue_head_prompt: Option<&'a str>,
284 pub queue_head_state: &'a str,
285 pub response_commit: Option<&'a str>,
286 pub mutations: Vec<SessionActorCloseoutMutation<'a>>,
287}
288
289pub fn state_db_path(project_root: &Path) -> PathBuf {
294 project_root.join(".agent-doc").join(STATE_DB_FILE)
295}
296
297pub fn open_state_db(project_root: &Path) -> Result<Connection> {
298 let path = state_db_path(project_root);
299 if let Some(parent) = path.parent() {
300 std::fs::create_dir_all(parent)?;
301 }
302 let conn =
303 Connection::open(&path).with_context(|| format!("failed to open {}", path.display()))?;
304 initialize_state_db(&conn)?;
305 Ok(conn)
306}
307
308pub fn initialize_state_db(conn: &Connection) -> Result<()> {
309 conn.execute_batch(
310 r#"
311 PRAGMA foreign_keys = ON;
312 PRAGMA journal_mode = WAL;
313 PRAGMA busy_timeout = 5000;
314
315 CREATE TABLE IF NOT EXISTS documents (
316 document_id TEXT PRIMARY KEY,
317 canonical_path TEXT NOT NULL,
318 session_id TEXT NOT NULL,
319 generation INTEGER NOT NULL,
320 pane_id TEXT NOT NULL,
321 window_id TEXT NOT NULL,
322 harness_id TEXT NOT NULL,
323 actor_state TEXT NOT NULL,
324 launch_mode TEXT,
325 controller_epoch INTEGER,
326 last_transition_id INTEGER
327 );
328
329 CREATE TABLE IF NOT EXISTS actor_transitions (
330 id INTEGER PRIMARY KEY AUTOINCREMENT,
331 document_id TEXT NOT NULL,
332 prior_generation INTEGER NOT NULL,
333 new_generation INTEGER NOT NULL,
334 caller TEXT NOT NULL,
335 reason TEXT NOT NULL,
336 old_pane TEXT,
337 new_pane TEXT NOT NULL,
338 old_window TEXT,
339 new_window TEXT,
340 timestamp INTEGER NOT NULL
341 );
342
343 CREATE TABLE IF NOT EXISTS supervisor_leases (
344 document_id TEXT NOT NULL,
345 generation INTEGER NOT NULL,
346 supervisor_pid INTEGER,
347 supervisor_socket TEXT,
348 last_heartbeat INTEGER,
349 runtime_state TEXT,
350 PRIMARY KEY (document_id, generation)
351 );
352
353 CREATE TABLE IF NOT EXISTS dispatch_attempts (
354 id INTEGER PRIMARY KEY AUTOINCREMENT,
355 document_id TEXT NOT NULL,
356 generation INTEGER NOT NULL,
357 command_kind TEXT NOT NULL,
358 accepted_stage TEXT,
359 failed_stage TEXT,
360 diagnostic_payload TEXT,
361 result_status TEXT,
362 proof_scope TEXT,
363 dispatch_start_proven INTEGER NOT NULL DEFAULT 0,
364 timestamp INTEGER NOT NULL
365 );
366
367 CREATE TABLE IF NOT EXISTS projection_diagnostics (
368 id INTEGER PRIMARY KEY AUTOINCREMENT,
369 projection TEXT NOT NULL,
370 document_id TEXT NOT NULL,
371 message TEXT NOT NULL,
372 timestamp INTEGER NOT NULL
373 );
374
375 CREATE TABLE IF NOT EXISTS queue_heads (
376 document_id TEXT NOT NULL,
377 queue_name TEXT NOT NULL,
378 generation INTEGER,
379 head_id TEXT,
380 prompt TEXT NOT NULL,
381 state TEXT NOT NULL,
382 priority INTEGER NOT NULL DEFAULT 0,
383 selected_at INTEGER NOT NULL,
384 updated_at INTEGER NOT NULL,
385 PRIMARY KEY (document_id, queue_name)
386 );
387
388 CREATE TABLE IF NOT EXISTS queue_controls (
389 id INTEGER PRIMARY KEY AUTOINCREMENT,
390 scope_kind TEXT NOT NULL,
391 scope_id TEXT NOT NULL,
392 state TEXT NOT NULL,
393 reason TEXT,
394 operation_receipt_id INTEGER,
395 updated_at INTEGER NOT NULL,
396 UNIQUE(scope_kind, scope_id)
397 );
398
399 CREATE TABLE IF NOT EXISTS queue_backpressure (
400 id INTEGER PRIMARY KEY AUTOINCREMENT,
401 document_id TEXT NOT NULL,
402 generation INTEGER,
403 command_kind TEXT NOT NULL,
404 capacity_class TEXT NOT NULL,
405 reason TEXT NOT NULL,
406 dispatch_receipt_id INTEGER,
407 timestamp INTEGER NOT NULL
408 );
409
410 CREATE TABLE IF NOT EXISTS document_cycles (
411 document_id TEXT NOT NULL,
412 cycle_id TEXT NOT NULL,
413 state TEXT NOT NULL,
414 queue_head_id TEXT,
415 response_commit TEXT,
416 updated_at INTEGER NOT NULL,
417 PRIMARY KEY (document_id, cycle_id)
418 );
419
420 CREATE TABLE IF NOT EXISTS pending_mutations (
421 id INTEGER PRIMARY KEY AUTOINCREMENT,
422 document_id TEXT NOT NULL,
423 cycle_id TEXT NOT NULL,
424 item_id TEXT NOT NULL,
425 mutation_kind TEXT NOT NULL,
426 status TEXT NOT NULL,
427 recorded_at INTEGER NOT NULL,
428 UNIQUE(document_id, cycle_id, item_id, mutation_kind)
429 );
430
431 CREATE TABLE IF NOT EXISTS admin_operations (
432 id INTEGER PRIMARY KEY AUTOINCREMENT,
433 operation_kind TEXT NOT NULL,
434 document_id TEXT,
435 status TEXT NOT NULL,
436 diagnostic_payload TEXT,
437 timestamp INTEGER NOT NULL
438 );
439
440 CREATE TABLE IF NOT EXISTS crash_recovery_markers (
441 id INTEGER PRIMARY KEY AUTOINCREMENT,
442 marker_kind TEXT NOT NULL,
443 document_id TEXT,
444 generation INTEGER,
445 status TEXT NOT NULL,
446 diagnostic_payload TEXT,
447 timestamp INTEGER NOT NULL
448 );
449
450 CREATE TABLE IF NOT EXISTS layout_states (
451 scope TEXT PRIMARY KEY,
452 columns_json TEXT NOT NULL,
453 updated_at INTEGER NOT NULL
454 );
455 "#,
456 )?;
457 ensure_dispatch_attempt_receipt_columns(conn)?;
458 ensure_projection_diagnostic_columns(conn)?;
459 ensure_queue_head_columns(conn)?;
460 Ok(())
461}
462
463fn ensure_column(conn: &Connection, table: &str, column: &str, definition: &str) -> Result<()> {
464 let mut stmt = conn.prepare(&format!("PRAGMA table_info({table})"))?;
465 let mut rows = stmt.query([])?;
466 while let Some(row) = rows.next()? {
467 let existing: String = row.get(1)?;
468 if existing == column {
469 return Ok(());
470 }
471 }
472 conn.execute(&format!("ALTER TABLE {table} ADD COLUMN {definition}"), [])?;
473 Ok(())
474}
475
476fn ensure_dispatch_attempt_receipt_columns(conn: &Connection) -> Result<()> {
477 ensure_column(
478 conn,
479 "dispatch_attempts",
480 "result_status",
481 "result_status TEXT",
482 )?;
483 ensure_column(conn, "dispatch_attempts", "proof_scope", "proof_scope TEXT")?;
484 ensure_column(
485 conn,
486 "dispatch_attempts",
487 "dispatch_start_proven",
488 "dispatch_start_proven INTEGER NOT NULL DEFAULT 0",
489 )?;
490 Ok(())
491}
492
493fn ensure_projection_diagnostic_columns(conn: &Connection) -> Result<()> {
494 ensure_column(
495 conn,
496 "projection_diagnostics",
497 "source_generation",
498 "source_generation INTEGER",
499 )?;
500 ensure_column(
501 conn,
502 "projection_diagnostics",
503 "intended_hash",
504 "intended_hash TEXT",
505 )?;
506 ensure_column(
507 conn,
508 "projection_diagnostics",
509 "retry_status",
510 "retry_status TEXT",
511 )?;
512 Ok(())
513}
514
515fn ensure_queue_head_columns(conn: &Connection) -> Result<()> {
516 ensure_column(conn, "queue_heads", "generation", "generation INTEGER")?;
517 ensure_column(
518 conn,
519 "queue_heads",
520 "priority",
521 "priority INTEGER NOT NULL DEFAULT 0",
522 )?;
523 Ok(())
524}
525
526pub fn sqlite_i64(value: u64, name: &str) -> Result<i64> {
531 i64::try_from(value).with_context(|| format!("{name} is too large for sqlite INTEGER"))
532}
533
534pub fn sqlite_u64(value: i64, name: &str) -> Result<u64> {
535 u64::try_from(value).with_context(|| format!("{name} is negative in sqlite state"))
536}
537
538pub fn timestamp_secs() -> u64 {
539 SystemTime::now()
540 .duration_since(UNIX_EPOCH)
541 .unwrap_or_default()
542 .as_secs()
543}
544
545fn count_rows(conn: &Connection, sql: &str, label: &str) -> Result<usize> {
546 let count: i64 = conn
547 .query_row(sql, [], |row| row.get(0))
548 .with_context(|| format!("failed to count {label} in controller state"))?;
549 usize::try_from(count).with_context(|| format!("{label} count is negative"))
550}
551
552pub fn load_control_plane_store_counts(conn: &Connection) -> Result<ControlPlaneStoreCounts> {
553 Ok(ControlPlaneStoreCounts {
554 actor_documents: count_rows(conn, "SELECT COUNT(*) FROM documents", "actor documents")?,
555 live_actor_documents: count_rows(
556 conn,
557 "SELECT COUNT(*) FROM documents WHERE actor_state != 'closed'",
558 "live actor documents",
559 )?,
560 actor_transitions: count_rows(
561 conn,
562 "SELECT COUNT(*) FROM actor_transitions",
563 "actor transitions",
564 )?,
565 supervisor_leases: count_rows(
566 conn,
567 "SELECT COUNT(*) FROM supervisor_leases",
568 "supervisor leases",
569 )?,
570 dispatch_receipts: count_rows(
571 conn,
572 "SELECT COUNT(*) FROM dispatch_attempts",
573 "dispatch attempts",
574 )?,
575 queue_heads: count_rows(conn, "SELECT COUNT(*) FROM queue_heads", "queue heads")?,
576 document_cycles: count_rows(
577 conn,
578 "SELECT COUNT(*) FROM document_cycles",
579 "document cycles",
580 )?,
581 pending_mutations: count_rows(
582 conn,
583 "SELECT COUNT(*) FROM pending_mutations",
584 "pending mutations",
585 )?,
586 projection_diagnostics: count_rows(
587 conn,
588 "SELECT COUNT(*) FROM projection_diagnostics",
589 "projection diagnostics",
590 )?,
591 admin_operations: count_rows(
592 conn,
593 "SELECT COUNT(*) FROM admin_operations",
594 "admin operations",
595 )?,
596 queue_controls: count_rows(
597 conn,
598 "SELECT COUNT(*) FROM queue_controls",
599 "queue controls",
600 )?,
601 queue_backpressure: count_rows(
602 conn,
603 "SELECT COUNT(*) FROM queue_backpressure",
604 "queue backpressure",
605 )?,
606 crash_recovery_markers: count_rows(
607 conn,
608 "SELECT COUNT(*) FROM crash_recovery_markers",
609 "crash recovery markers",
610 )?,
611 layout_states: count_rows(conn, "SELECT COUNT(*) FROM layout_states", "layout states")?,
612 })
613}
614
615pub fn actor_record_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<ActorRecord> {
620 let actor_state: String = row.get("actor_state")?;
621 let state = ActorState::parse(&actor_state).ok_or(rusqlite::Error::InvalidQuery)?;
622 let generation: i64 = row.get("generation")?;
623 let transition_prior: i64 = row.get("prior_generation")?;
624 let transition_new: i64 = row.get("new_generation")?;
625 let transition_timestamp: i64 = row.get("timestamp")?;
626 Ok(ActorRecord {
627 document_id: row.get("document_id")?,
628 session_id: row.get("session_id")?,
629 generation: sqlite_u64(generation, "generation")
630 .map_err(|_| rusqlite::Error::InvalidQuery)?,
631 pane_id: row.get("pane_id")?,
632 window_id: row.get("window_id")?,
633 harness: row.get("harness_id")?,
634 state,
635 last_transition: ActorLastTransition {
636 caller: row.get("caller")?,
637 reason: row.get("reason")?,
638 timestamp: sqlite_u64(transition_timestamp, "transition timestamp")
639 .map_err(|_| rusqlite::Error::InvalidQuery)?,
640 prior_generation: sqlite_u64(transition_prior, "transition prior_generation")
641 .map_err(|_| rusqlite::Error::InvalidQuery)?,
642 new_generation: sqlite_u64(transition_new, "transition new_generation")
643 .map_err(|_| rusqlite::Error::InvalidQuery)?,
644 },
645 })
646}
647
648pub fn load_actor_record_from_db(
649 conn: &Connection,
650 document_id: &str,
651) -> Result<Option<ActorRecord>> {
652 conn.query_row(
653 r#"
654 SELECT
655 d.document_id,
656 d.session_id,
657 d.generation,
658 d.pane_id,
659 d.window_id,
660 d.harness_id,
661 d.actor_state,
662 t.caller,
663 t.reason,
664 t.timestamp,
665 t.prior_generation,
666 t.new_generation
667 FROM documents d
668 JOIN actor_transitions t ON t.id = d.last_transition_id
669 WHERE d.document_id = ?1
670 "#,
671 params![document_id],
672 actor_record_from_row,
673 )
674 .optional()
675 .context("failed to load actor record from controller state")
676}
677
678pub fn load_actor_store_from_db(conn: &Connection) -> Result<BTreeMap<String, ActorRecord>> {
679 let mut stmt = conn.prepare(
680 r#"
681 SELECT
682 d.document_id,
683 d.session_id,
684 d.generation,
685 d.pane_id,
686 d.window_id,
687 d.harness_id,
688 d.actor_state,
689 t.caller,
690 t.reason,
691 t.timestamp,
692 t.prior_generation,
693 t.new_generation
694 FROM documents d
695 JOIN actor_transitions t ON t.id = d.last_transition_id
696 ORDER BY d.document_id
697 "#,
698 )?;
699 let mut store = BTreeMap::new();
700 for row in stmt.query_map([], actor_record_from_row)? {
701 let record = row?;
702 store.insert(record.document_id.clone(), record);
703 }
704 Ok(store)
705}
706
707pub fn load_actor_transitions_from_db(
708 conn: &Connection,
709 document_id: &str,
710) -> Result<Vec<ActorTransitionStatus>> {
711 let mut stmt = conn.prepare(
712 r#"
713 SELECT
714 prior_generation,
715 new_generation,
716 caller,
717 reason,
718 old_pane,
719 new_pane,
720 old_window,
721 new_window,
722 timestamp
723 FROM actor_transitions
724 WHERE document_id = ?1
725 ORDER BY id
726 "#,
727 )?;
728 let mut transitions = Vec::new();
729 for row in stmt.query_map(params![document_id], |row| {
730 let prior_generation: i64 = row.get("prior_generation")?;
731 let new_generation: i64 = row.get("new_generation")?;
732 let timestamp: i64 = row.get("timestamp")?;
733 Ok(ActorTransitionStatus {
734 prior_generation: sqlite_u64(prior_generation, "transition prior_generation")
735 .map_err(|_| rusqlite::Error::InvalidQuery)?,
736 new_generation: sqlite_u64(new_generation, "transition new_generation")
737 .map_err(|_| rusqlite::Error::InvalidQuery)?,
738 caller: row.get("caller")?,
739 reason: row.get("reason")?,
740 old_pane: row.get("old_pane")?,
741 new_pane: row.get("new_pane")?,
742 old_window: row.get("old_window")?,
743 new_window: row.get("new_window")?,
744 timestamp: sqlite_u64(timestamp, "transition timestamp")
745 .map_err(|_| rusqlite::Error::InvalidQuery)?,
746 })
747 })? {
748 transitions.push(row?);
749 }
750 Ok(transitions)
751}
752
753pub fn load_supervisor_lease_from_db(
754 conn: &Connection,
755 document_id: &str,
756 generation: u64,
757) -> Result<Option<SupervisorLeaseStatus>> {
758 conn.query_row(
759 r#"
760 SELECT
761 generation,
762 supervisor_pid,
763 supervisor_socket,
764 last_heartbeat,
765 runtime_state
766 FROM supervisor_leases
767 WHERE document_id = ?1 AND generation = ?2
768 "#,
769 params![document_id, sqlite_i64(generation, "generation")?],
770 |row| {
771 let generation: i64 = row.get("generation")?;
772 let supervisor_pid: Option<i64> = row.get("supervisor_pid")?;
773 let last_heartbeat: Option<i64> = row.get("last_heartbeat")?;
774 Ok(SupervisorLeaseStatus {
775 generation: sqlite_u64(generation, "supervisor lease generation")
776 .map_err(|_| rusqlite::Error::InvalidQuery)?,
777 supervisor_pid: supervisor_pid.and_then(|pid| u32::try_from(pid).ok()),
778 supervisor_socket: row.get("supervisor_socket")?,
779 last_heartbeat: last_heartbeat
780 .map(|value| sqlite_u64(value, "supervisor last heartbeat"))
781 .transpose()
782 .map_err(|_| rusqlite::Error::InvalidQuery)?,
783 runtime_state: row.get("runtime_state")?,
784 })
785 },
786 )
787 .optional()
788 .context("failed to load supervisor lease from controller state")
789}
790
791pub fn load_dispatch_attempts_from_db(
792 conn: &Connection,
793 document_id: &str,
794) -> Result<Vec<DispatchAttemptStatus>> {
795 let mut stmt = conn.prepare(
796 r#"
797 SELECT
798 id,
799 generation,
800 command_kind,
801 accepted_stage,
802 failed_stage,
803 diagnostic_payload,
804 result_status,
805 proof_scope,
806 dispatch_start_proven,
807 timestamp
808 FROM dispatch_attempts
809 WHERE document_id = ?1
810 ORDER BY id DESC
811 LIMIT 10
812 "#,
813 )?;
814 let mut attempts = Vec::new();
815 for row in stmt.query_map(params![document_id], |row| {
816 let receipt_id: i64 = row.get("id")?;
817 let generation: i64 = row.get("generation")?;
818 let dispatch_start_proven: i64 = row.get("dispatch_start_proven")?;
819 let timestamp: i64 = row.get("timestamp")?;
820 Ok(DispatchAttemptStatus {
821 receipt_id: sqlite_u64(receipt_id, "dispatch receipt id")
822 .map_err(|_| rusqlite::Error::InvalidQuery)?,
823 generation: sqlite_u64(generation, "dispatch generation")
824 .map_err(|_| rusqlite::Error::InvalidQuery)?,
825 command_kind: row.get("command_kind")?,
826 accepted_stage: row.get("accepted_stage")?,
827 failed_stage: row.get("failed_stage")?,
828 diagnostic_payload: row.get("diagnostic_payload")?,
829 result_status: row.get("result_status")?,
830 proof_scope: row.get("proof_scope")?,
831 dispatch_start_proven: dispatch_start_proven != 0,
832 timestamp: sqlite_u64(timestamp, "dispatch timestamp")
833 .map_err(|_| rusqlite::Error::InvalidQuery)?,
834 })
835 })? {
836 attempts.push(row?);
837 }
838 attempts.reverse();
839 Ok(attempts)
840}
841
842pub fn load_projection_diagnostics_from_db(
843 conn: &Connection,
844 document_id: &str,
845) -> Result<Vec<ProjectionDiagnosticStatus>> {
846 let mut stmt = conn.prepare(
847 r#"
848 SELECT projection, message, source_generation, intended_hash, retry_status, timestamp
849 FROM projection_diagnostics
850 WHERE document_id = ?1
851 ORDER BY id DESC
852 LIMIT 10
853 "#,
854 )?;
855 let mut diagnostics = Vec::new();
856 for row in stmt.query_map(params![document_id], |row| {
857 let timestamp: i64 = row.get("timestamp")?;
858 let source_generation: Option<i64> = row.get("source_generation")?;
859 Ok(ProjectionDiagnosticStatus {
860 projection: row.get("projection")?,
861 message: row.get("message")?,
862 source_generation: source_generation
863 .map(|generation| sqlite_u64(generation, "projection source generation"))
864 .transpose()
865 .map_err(|_| rusqlite::Error::InvalidQuery)?,
866 intended_hash: row.get("intended_hash")?,
867 retry_status: row.get("retry_status")?,
868 timestamp: sqlite_u64(timestamp, "projection diagnostic timestamp")
869 .map_err(|_| rusqlite::Error::InvalidQuery)?,
870 })
871 })? {
872 diagnostics.push(row?);
873 }
874 diagnostics.reverse();
875 Ok(diagnostics)
876}
877
878pub fn load_queue_head_from_db(
879 conn: &Connection,
880 document_id: &str,
881 queue_name: &str,
882) -> Result<Option<QueueHeadStatus>> {
883 conn.query_row(
884 r#"
885 SELECT
886 document_id,
887 queue_name,
888 generation,
889 head_id,
890 prompt,
891 state,
892 priority,
893 selected_at,
894 updated_at
895 FROM queue_heads
896 WHERE document_id = ?1 AND queue_name = ?2
897 "#,
898 params![document_id, queue_name],
899 |row| {
900 let generation: Option<i64> = row.get("generation")?;
901 let priority: i64 = row.get("priority")?;
902 let selected_at: i64 = row.get("selected_at")?;
903 let updated_at: i64 = row.get("updated_at")?;
904 Ok(QueueHeadStatus {
905 document_id: row.get("document_id")?,
906 queue_name: row.get("queue_name")?,
907 generation: generation
908 .map(|generation| sqlite_u64(generation, "queue head generation"))
909 .transpose()
910 .map_err(|_| rusqlite::Error::InvalidQuery)?,
911 head_id: row.get("head_id")?,
912 prompt: row.get("prompt")?,
913 state: row.get("state")?,
914 priority: sqlite_u64(priority, "queue head priority")
915 .map_err(|_| rusqlite::Error::InvalidQuery)?,
916 selected_at: sqlite_u64(selected_at, "queue head selected_at")
917 .map_err(|_| rusqlite::Error::InvalidQuery)?,
918 updated_at: sqlite_u64(updated_at, "queue head updated_at")
919 .map_err(|_| rusqlite::Error::InvalidQuery)?,
920 })
921 },
922 )
923 .optional()
924 .context("failed to load queue head from controller state")
925}
926
927pub fn load_queue_control_from_db(
928 conn: &Connection,
929 scope_kind: &str,
930 scope_id: &str,
931) -> Result<Option<QueueControlStatus>> {
932 conn.query_row(
933 r#"
934 SELECT
935 id,
936 scope_kind,
937 scope_id,
938 state,
939 reason,
940 operation_receipt_id,
941 updated_at
942 FROM queue_controls
943 WHERE scope_kind = ?1 AND scope_id = ?2
944 "#,
945 params![scope_kind, scope_id],
946 |row| {
947 let receipt_id: i64 = row.get("id")?;
948 let operation_receipt_id: Option<i64> = row.get("operation_receipt_id")?;
949 let updated_at: i64 = row.get("updated_at")?;
950 Ok(QueueControlStatus {
951 receipt_id: sqlite_u64(receipt_id, "queue control receipt id")
952 .map_err(|_| rusqlite::Error::InvalidQuery)?,
953 scope_kind: row.get("scope_kind")?,
954 scope_id: row.get("scope_id")?,
955 state: row.get("state")?,
956 reason: row.get("reason")?,
957 operation_receipt_id: operation_receipt_id
958 .map(|value| sqlite_u64(value, "queue control operation receipt id"))
959 .transpose()
960 .map_err(|_| rusqlite::Error::InvalidQuery)?,
961 updated_at: sqlite_u64(updated_at, "queue control updated_at")
962 .map_err(|_| rusqlite::Error::InvalidQuery)?,
963 })
964 },
965 )
966 .optional()
967 .context("failed to load queue control from controller state")
968}
969
970pub fn load_effective_queue_control_from_db(
971 conn: &Connection,
972 document_id: &str,
973 project_scope_id: &str,
974) -> Result<Option<QueueControlStatus>> {
975 if let Some(control) = load_queue_control_from_db(conn, "document", document_id)?
976 && control.state != "resumed"
977 {
978 return Ok(Some(control));
979 }
980 let project = load_queue_control_from_db(conn, "project", project_scope_id)?;
981 Ok(project.filter(|control| control.state != "resumed"))
982}
983
984pub fn load_admin_operations_from_db(
985 conn: &Connection,
986 document_id: Option<&str>,
987 limit: usize,
988) -> Result<Vec<AdminOperationStatus>> {
989 let limit = i64::try_from(limit.max(1)).context("admin operation limit too large")?;
990 let mut operations = Vec::new();
991 if let Some(document_id) = document_id {
992 let mut stmt = conn.prepare(
993 r#"
994 SELECT id, operation_kind, document_id, status, diagnostic_payload, timestamp
995 FROM admin_operations
996 WHERE document_id = ?1
997 ORDER BY id DESC
998 LIMIT ?2
999 "#,
1000 )?;
1001 for row in stmt.query_map(params![document_id, limit], admin_operation_from_row)? {
1002 operations.push(row?);
1003 }
1004 } else {
1005 let mut stmt = conn.prepare(
1006 r#"
1007 SELECT id, operation_kind, document_id, status, diagnostic_payload, timestamp
1008 FROM admin_operations
1009 ORDER BY id DESC
1010 LIMIT ?1
1011 "#,
1012 )?;
1013 for row in stmt.query_map(params![limit], admin_operation_from_row)? {
1014 operations.push(row?);
1015 }
1016 }
1017 operations.reverse();
1018 Ok(operations)
1019}
1020
1021pub fn load_queue_backpressure_from_db(
1022 conn: &Connection,
1023 document_id: &str,
1024 limit: usize,
1025) -> Result<Vec<QueueBackpressureStatus>> {
1026 let limit = i64::try_from(limit.max(1)).context("queue backpressure limit too large")?;
1027 let mut stmt = conn.prepare(
1028 r#"
1029 SELECT
1030 id,
1031 document_id,
1032 generation,
1033 command_kind,
1034 capacity_class,
1035 reason,
1036 dispatch_receipt_id,
1037 timestamp
1038 FROM queue_backpressure
1039 WHERE document_id = ?1
1040 ORDER BY id DESC
1041 LIMIT ?2
1042 "#,
1043 )?;
1044 let mut receipts = Vec::new();
1045 for row in stmt.query_map(params![document_id, limit], |row| {
1046 let receipt_id: i64 = row.get("id")?;
1047 let generation: Option<i64> = row.get("generation")?;
1048 let dispatch_receipt_id: Option<i64> = row.get("dispatch_receipt_id")?;
1049 let timestamp: i64 = row.get("timestamp")?;
1050 Ok(QueueBackpressureStatus {
1051 receipt_id: sqlite_u64(receipt_id, "queue backpressure receipt id")
1052 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1053 document_id: row.get("document_id")?,
1054 generation: generation
1055 .map(|generation| sqlite_u64(generation, "queue backpressure generation"))
1056 .transpose()
1057 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1058 command_kind: row.get("command_kind")?,
1059 capacity_class: row.get("capacity_class")?,
1060 reason: row.get("reason")?,
1061 dispatch_receipt_id: dispatch_receipt_id
1062 .map(|receipt_id| sqlite_u64(receipt_id, "queue backpressure dispatch receipt id"))
1063 .transpose()
1064 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1065 timestamp: sqlite_u64(timestamp, "queue backpressure timestamp")
1066 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1067 })
1068 })? {
1069 receipts.push(row?);
1070 }
1071 receipts.reverse();
1072 Ok(receipts)
1073}
1074
1075fn admin_operation_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<AdminOperationStatus> {
1076 let receipt_id: i64 = row.get("id")?;
1077 let timestamp: i64 = row.get("timestamp")?;
1078 Ok(AdminOperationStatus {
1079 receipt_id: sqlite_u64(receipt_id, "admin operation receipt id")
1080 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1081 operation_kind: row.get("operation_kind")?,
1082 document_id: row.get("document_id")?,
1083 status: row.get("status")?,
1084 diagnostic_payload: row.get("diagnostic_payload")?,
1085 timestamp: sqlite_u64(timestamp, "admin operation timestamp")
1086 .map_err(|_| rusqlite::Error::InvalidQuery)?,
1087 })
1088}
1089
1090pub fn load_session_operator_status_from_db(
1091 conn: &Connection,
1092 document_id: &str,
1093) -> Result<SessionOperatorStatus> {
1094 let record = load_actor_record_from_db(conn, document_id)?;
1095 let supervisor_lease = match &record {
1096 Some(record) => load_supervisor_lease_from_db(conn, document_id, record.generation)?,
1097 None => None,
1098 };
1099 Ok(SessionOperatorStatus {
1100 record,
1101 transitions: load_actor_transitions_from_db(conn, document_id)?,
1102 supervisor_lease,
1103 dispatch_attempts: load_dispatch_attempts_from_db(conn, document_id)?,
1104 projection_diagnostics: load_projection_diagnostics_from_db(conn, document_id)?,
1105 })
1106}
1107
1108pub fn insert_actor_transition(
1113 conn: &Connection,
1114 previous: Option<&ActorRecord>,
1115 record: &ActorRecord,
1116) -> Result<i64> {
1117 conn.execute(
1118 r#"
1119 INSERT INTO actor_transitions (
1120 document_id,
1121 prior_generation,
1122 new_generation,
1123 caller,
1124 reason,
1125 old_pane,
1126 new_pane,
1127 old_window,
1128 new_window,
1129 timestamp
1130 )
1131 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
1132 "#,
1133 params![
1134 record.document_id,
1135 sqlite_i64(record.last_transition.prior_generation, "prior_generation")?,
1136 sqlite_i64(record.last_transition.new_generation, "new_generation")?,
1137 record.last_transition.caller,
1138 record.last_transition.reason,
1139 previous.map(|prior| prior.pane_id.as_str()),
1140 record.pane_id,
1141 previous.map(|prior| prior.window_id.as_str()),
1142 record.window_id,
1143 sqlite_i64(record.last_transition.timestamp, "transition timestamp")?,
1144 ],
1145 )?;
1146 Ok(conn.last_insert_rowid())
1147}
1148
1149pub fn upsert_actor_document(
1156 conn: &Connection,
1157 record: &ActorRecord,
1158 transition_id: i64,
1159 launch_mode: Option<String>,
1160 controller_epoch: Option<i64>,
1161) -> Result<()> {
1162 conn.execute(
1163 r#"
1164 INSERT INTO documents (
1165 document_id,
1166 canonical_path,
1167 session_id,
1168 generation,
1169 pane_id,
1170 window_id,
1171 harness_id,
1172 actor_state,
1173 launch_mode,
1174 controller_epoch,
1175 last_transition_id
1176 )
1177 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
1178 ON CONFLICT(document_id) DO UPDATE SET
1179 canonical_path = excluded.canonical_path,
1180 session_id = excluded.session_id,
1181 generation = excluded.generation,
1182 pane_id = excluded.pane_id,
1183 window_id = excluded.window_id,
1184 harness_id = excluded.harness_id,
1185 actor_state = excluded.actor_state,
1186 launch_mode = excluded.launch_mode,
1187 controller_epoch = excluded.controller_epoch,
1188 last_transition_id = excluded.last_transition_id
1189 "#,
1190 params![
1191 record.document_id,
1192 record.document_id,
1193 record.session_id,
1194 sqlite_i64(record.generation, "generation")?,
1195 record.pane_id,
1196 record.window_id,
1197 record.harness,
1198 record.state.as_str(),
1199 launch_mode,
1200 controller_epoch,
1201 transition_id,
1202 ],
1203 )?;
1204 Ok(())
1205}
1206
1207pub fn upsert_supervisor_lease_in_db(
1208 conn: &Connection,
1209 record: &ActorRecord,
1210 supervisor_pid: Option<u32>,
1211 supervisor_socket: Option<&str>,
1212 runtime_state: &str,
1213) -> Result<()> {
1214 conn.execute(
1215 r#"
1216 INSERT INTO supervisor_leases (
1217 document_id,
1218 generation,
1219 supervisor_pid,
1220 supervisor_socket,
1221 last_heartbeat,
1222 runtime_state
1223 )
1224 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1225 ON CONFLICT(document_id, generation) DO UPDATE SET
1226 supervisor_pid = COALESCE(excluded.supervisor_pid, supervisor_leases.supervisor_pid),
1227 supervisor_socket = COALESCE(excluded.supervisor_socket, supervisor_leases.supervisor_socket),
1228 last_heartbeat = excluded.last_heartbeat,
1229 runtime_state = excluded.runtime_state
1230 "#,
1231 params![
1232 record.document_id,
1233 sqlite_i64(record.generation, "generation")?,
1234 supervisor_pid.map(i64::from),
1235 supervisor_socket,
1236 sqlite_i64(timestamp_secs(), "supervisor heartbeat timestamp")?,
1237 runtime_state,
1238 ],
1239 )?;
1240 Ok(())
1241}
1242
1243pub fn has_open_in_flight_dispatch(
1248 conn: &Connection,
1249 document_id: &str,
1250 generation: u64,
1251) -> Result<bool> {
1252 let count: i64 = conn.query_row(
1253 r#"
1254 SELECT COUNT(*)
1255 FROM dispatch_attempts
1256 WHERE document_id = ?1
1257 AND generation = ?2
1258 AND failed_stage IS NULL
1259 AND COALESCE(result_status, '') IN ('accepted', 'queued', 'running')
1260 AND dispatch_start_proven = 0
1261 "#,
1262 params![document_id, generation as i64],
1263 |row| row.get(0),
1264 )?;
1265 Ok(count > 0)
1266}
1267
1268pub fn has_any_open_in_flight_dispatch(conn: &Connection) -> Result<bool> {
1274 let count: i64 = conn.query_row(
1275 r#"
1276 SELECT COUNT(*)
1277 FROM dispatch_attempts
1278 WHERE failed_stage IS NULL
1279 AND COALESCE(result_status, '') IN ('accepted', 'queued', 'running')
1280 AND dispatch_start_proven = 0
1281 "#,
1282 [],
1283 |row| row.get(0),
1284 )?;
1285 Ok(count > 0)
1286}
1287
1288pub fn mark_open_dispatches_consumed(conn: &Connection, document_id: &str) -> Result<usize> {
1293 let released = conn.execute(
1294 r#"
1295 UPDATE dispatch_attempts
1296 SET dispatch_start_proven = 1
1297 WHERE document_id = ?1
1298 AND failed_stage IS NULL
1299 AND COALESCE(result_status, '') IN ('accepted', 'queued', 'running')
1300 AND dispatch_start_proven = 0
1301 "#,
1302 params![document_id],
1303 )?;
1304 Ok(released)
1305}
1306
1307pub fn insert_dispatch_attempt_in_db(
1308 conn: &Connection,
1309 attempt: &DispatchAttemptInsert<'_>,
1310) -> Result<u64> {
1311 conn.execute(
1312 r#"
1313 INSERT INTO dispatch_attempts (
1314 document_id,
1315 generation,
1316 command_kind,
1317 accepted_stage,
1318 failed_stage,
1319 diagnostic_payload,
1320 result_status,
1321 proof_scope,
1322 dispatch_start_proven,
1323 timestamp
1324 )
1325 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
1326 "#,
1327 params![
1328 attempt.document_id,
1329 sqlite_i64(attempt.generation, "generation")?,
1330 attempt.command_kind,
1331 attempt.accepted_stage,
1332 attempt.failed_stage,
1333 attempt.diagnostic_payload,
1334 attempt.result_status,
1335 attempt.proof_scope,
1336 if attempt.dispatch_start_proven {
1337 1_i64
1338 } else {
1339 0_i64
1340 },
1341 sqlite_i64(timestamp_secs(), "dispatch attempt timestamp")?,
1342 ],
1343 )?;
1344 sqlite_u64(conn.last_insert_rowid(), "dispatch receipt id")
1345}
1346
1347pub fn insert_projection_diagnostic(
1348 conn: &Connection,
1349 projection: &str,
1350 document_id: &str,
1351 message: &str,
1352) -> Result<()> {
1353 insert_projection_diagnostic_with_metadata(
1354 conn,
1355 &ProjectionDiagnosticInsert {
1356 projection,
1357 document_id,
1358 message,
1359 source_generation: None,
1360 intended_hash: None,
1361 retry_status: "retry_pending",
1362 },
1363 )
1364}
1365
1366pub fn insert_projection_diagnostic_with_metadata(
1367 conn: &Connection,
1368 diagnostic: &ProjectionDiagnosticInsert<'_>,
1369) -> Result<()> {
1370 conn.execute(
1371 r#"
1372 INSERT INTO projection_diagnostics (
1373 projection,
1374 document_id,
1375 message,
1376 source_generation,
1377 intended_hash,
1378 retry_status,
1379 timestamp
1380 )
1381 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
1382 "#,
1383 params![
1384 diagnostic.projection,
1385 diagnostic.document_id,
1386 diagnostic.message,
1387 diagnostic
1388 .source_generation
1389 .map(|generation| sqlite_i64(generation, "projection source generation"))
1390 .transpose()?,
1391 diagnostic.intended_hash,
1392 diagnostic.retry_status,
1393 sqlite_i64(timestamp_secs(), "projection diagnostic timestamp")?
1394 ],
1395 )?;
1396 Ok(())
1397}
1398
1399fn evict_cross_document_actor_pane_bindings_tx(
1400 conn: &Connection,
1401 owner_document_id: &str,
1402 pane_id: &str,
1403 caller: &str,
1404 timestamp: u64,
1405 launch_mode: Option<String>,
1406 controller_epoch: Option<i64>,
1407) -> Result<Vec<String>> {
1408 if pane_id.is_empty() {
1409 return Ok(Vec::new());
1410 }
1411 let store = load_actor_store_from_db(conn)?;
1412 let mut evicted = Vec::new();
1413 for prior in store.values() {
1414 if prior.document_id == owner_document_id || prior.pane_id != pane_id {
1415 continue;
1416 }
1417 let mut next = prior.clone();
1418 next.state = ActorState::Closed;
1419 next.pane_id.clear();
1420 next.window_id.clear();
1421 next.last_transition = ActorLastTransition {
1422 caller: caller.to_string(),
1423 reason: format!("evicted_cross_document_pane owner={owner_document_id} pane={pane_id}"),
1424 timestamp,
1425 prior_generation: prior.generation,
1426 new_generation: prior.generation,
1427 };
1428 let transition_id = insert_actor_transition(conn, Some(prior), &next)?;
1429 upsert_actor_document(
1430 conn,
1431 &next,
1432 transition_id,
1433 launch_mode.clone(),
1434 controller_epoch,
1435 )?;
1436 evicted.push(prior.document_id.clone());
1437 }
1438 Ok(evicted)
1439}
1440
1441pub fn store_actor_record_tx(
1449 conn: &mut Connection,
1450 expected_prior_generation: Option<u64>,
1451 record: &ActorRecord,
1452 launch_mode: Option<String>,
1453 controller_epoch: Option<i64>,
1454) -> Result<Vec<String>> {
1455 let tx = conn.transaction()?;
1456 let previous = load_actor_record_from_db(&tx, &record.document_id)?;
1457 let prior_generation = previous
1458 .as_ref()
1459 .map(|prior| prior.generation)
1460 .unwrap_or(record.last_transition.prior_generation);
1461 if let Some(expected) = expected_prior_generation
1462 && prior_generation != expected
1463 {
1464 anyhow::bail!(
1465 "controller actor generation compare-and-swap failed for {}: expected {}, found {}",
1466 record.document_id,
1467 expected,
1468 prior_generation
1469 );
1470 }
1471 if record.generation < prior_generation {
1472 anyhow::bail!(
1473 "controller actor generation regression for {}: attempted {}, current {}",
1474 record.document_id,
1475 record.generation,
1476 prior_generation
1477 );
1478 }
1479 let evicted_document_ids = if record.state != ActorState::Closed {
1480 evict_cross_document_actor_pane_bindings_tx(
1481 &tx,
1482 &record.document_id,
1483 &record.pane_id,
1484 &record.last_transition.caller,
1485 record.last_transition.timestamp,
1486 launch_mode.clone(),
1487 controller_epoch,
1488 )?
1489 } else {
1490 Vec::new()
1491 };
1492 let transition_id = insert_actor_transition(&tx, previous.as_ref(), record)?;
1493 upsert_actor_document(&tx, record, transition_id, launch_mode, controller_epoch)?;
1494 tx.commit()?;
1495 Ok(evicted_document_ids)
1496}
1497
1498pub fn actor_documents_empty(conn: &Connection) -> Result<bool> {
1503 let count: i64 = conn.query_row("SELECT COUNT(*) FROM documents", [], |row| row.get(0))?;
1504 Ok(count == 0)
1505}
1506
1507pub fn migrate_actor_store_tx(
1513 conn: &mut Connection,
1514 store: &BTreeMap<String, ActorRecord>,
1515 launch_mode: Option<String>,
1516 controller_epoch: Option<i64>,
1517) -> Result<()> {
1518 let count: i64 = conn.query_row("SELECT COUNT(*) FROM documents", [], |row| row.get(0))?;
1519 if count > 0 {
1520 return Ok(());
1521 }
1522 let tx = conn.transaction()?;
1523 for record in store.values() {
1524 let transition_id = insert_actor_transition(&tx, None, record)?;
1525 upsert_actor_document(
1526 &tx,
1527 record,
1528 transition_id,
1529 launch_mode.clone(),
1530 controller_epoch,
1531 )?;
1532 }
1533 tx.commit()?;
1534 Ok(())
1535}
1536
1537pub fn upsert_queue_head_in_db(
1542 conn: &Connection,
1543 document_id: &str,
1544 queue_name: &str,
1545 head_id: Option<&str>,
1546 prompt: &str,
1547 state: &str,
1548) -> Result<()> {
1549 let now = sqlite_i64(timestamp_secs(), "queue head timestamp")?;
1550 conn.execute(
1551 r#"
1552 INSERT INTO queue_heads (
1553 document_id,
1554 queue_name,
1555 head_id,
1556 prompt,
1557 state,
1558 selected_at,
1559 updated_at
1560 )
1561 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6)
1562 ON CONFLICT(document_id, queue_name) DO UPDATE SET
1563 head_id = excluded.head_id,
1564 prompt = excluded.prompt,
1565 state = excluded.state,
1566 updated_at = excluded.updated_at
1567 "#,
1568 params![document_id, queue_name, head_id, prompt, state, now],
1569 )?;
1570 Ok(())
1571}
1572
1573pub fn upsert_queue_control_in_db(
1574 conn: &Connection,
1575 control: &QueueControlInsert<'_>,
1576) -> Result<QueueControlStatus> {
1577 let now = sqlite_i64(timestamp_secs(), "queue control timestamp")?;
1578 conn.execute(
1579 r#"
1580 INSERT INTO queue_controls (
1581 scope_kind,
1582 scope_id,
1583 state,
1584 reason,
1585 operation_receipt_id,
1586 updated_at
1587 )
1588 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1589 ON CONFLICT(scope_kind, scope_id) DO UPDATE SET
1590 state = excluded.state,
1591 reason = excluded.reason,
1592 operation_receipt_id = excluded.operation_receipt_id,
1593 updated_at = excluded.updated_at
1594 "#,
1595 params![
1596 control.scope_kind,
1597 control.scope_id,
1598 control.state,
1599 control.reason,
1600 control
1601 .operation_receipt_id
1602 .map(|receipt_id| sqlite_i64(receipt_id, "queue control operation receipt id"))
1603 .transpose()?,
1604 now
1605 ],
1606 )?;
1607 load_queue_control_from_db(conn, control.scope_kind, control.scope_id)?
1608 .context("missing queue control after upsert")
1609}
1610
1611pub fn insert_queue_backpressure_in_db(
1612 conn: &Connection,
1613 backpressure: &QueueBackpressureInsert<'_>,
1614) -> Result<QueueBackpressureStatus> {
1615 conn.execute(
1616 r#"
1617 INSERT INTO queue_backpressure (
1618 document_id,
1619 generation,
1620 command_kind,
1621 capacity_class,
1622 reason,
1623 dispatch_receipt_id,
1624 timestamp
1625 )
1626 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
1627 "#,
1628 params![
1629 backpressure.document_id,
1630 backpressure
1631 .generation
1632 .map(|generation| sqlite_i64(generation, "queue backpressure generation"))
1633 .transpose()?,
1634 backpressure.command_kind,
1635 backpressure.capacity_class,
1636 backpressure.reason,
1637 backpressure
1638 .dispatch_receipt_id
1639 .map(|receipt_id| sqlite_i64(receipt_id, "queue backpressure dispatch receipt id"))
1640 .transpose()?,
1641 sqlite_i64(timestamp_secs(), "queue backpressure timestamp")?
1642 ],
1643 )?;
1644 let receipt_id = sqlite_u64(conn.last_insert_rowid(), "queue backpressure receipt id")?;
1645 Ok(QueueBackpressureStatus {
1646 receipt_id,
1647 document_id: backpressure.document_id.to_string(),
1648 generation: backpressure.generation,
1649 command_kind: backpressure.command_kind.to_string(),
1650 capacity_class: backpressure.capacity_class.to_string(),
1651 reason: backpressure.reason.to_string(),
1652 dispatch_receipt_id: backpressure.dispatch_receipt_id,
1653 timestamp: timestamp_secs(),
1654 })
1655}
1656
1657pub fn upsert_document_cycle_state_in_db(
1658 conn: &Connection,
1659 document_id: &str,
1660 cycle_id: &str,
1661 state: &str,
1662 queue_head_id: Option<&str>,
1663 response_commit: Option<&str>,
1664) -> Result<()> {
1665 conn.execute(
1666 r#"
1667 INSERT INTO document_cycles (
1668 document_id,
1669 cycle_id,
1670 state,
1671 queue_head_id,
1672 response_commit,
1673 updated_at
1674 )
1675 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1676 ON CONFLICT(document_id, cycle_id) DO UPDATE SET
1677 state = excluded.state,
1678 queue_head_id = excluded.queue_head_id,
1679 response_commit = excluded.response_commit,
1680 updated_at = excluded.updated_at
1681 "#,
1682 params![
1683 document_id,
1684 cycle_id,
1685 state,
1686 queue_head_id,
1687 response_commit,
1688 sqlite_i64(timestamp_secs(), "document cycle timestamp")?
1689 ],
1690 )?;
1691 Ok(())
1692}
1693
1694pub fn commit_session_actor_closeout_in_db(
1695 conn: &mut Connection,
1696 closeout: &SessionActorCloseoutCommit<'_>,
1697) -> Result<()> {
1698 let now = sqlite_i64(timestamp_secs(), "session actor closeout timestamp")?;
1699 let tx = conn.transaction()?;
1700
1701 if let Some(prompt) = closeout.queue_head_prompt {
1702 tx.execute(
1703 r#"
1704 INSERT INTO queue_heads (
1705 document_id,
1706 queue_name,
1707 head_id,
1708 prompt,
1709 state,
1710 selected_at,
1711 updated_at
1712 )
1713 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6)
1714 ON CONFLICT(document_id, queue_name) DO UPDATE SET
1715 head_id = excluded.head_id,
1716 prompt = excluded.prompt,
1717 state = excluded.state,
1718 updated_at = excluded.updated_at
1719 "#,
1720 params![
1721 closeout.document_id,
1722 closeout.queue_name,
1723 closeout.queue_head_id,
1724 prompt,
1725 closeout.queue_head_state,
1726 now
1727 ],
1728 )?;
1729 }
1730
1731 tx.execute(
1732 r#"
1733 INSERT INTO document_cycles (
1734 document_id,
1735 cycle_id,
1736 state,
1737 queue_head_id,
1738 response_commit,
1739 updated_at
1740 )
1741 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1742 ON CONFLICT(document_id, cycle_id) DO UPDATE SET
1743 state = excluded.state,
1744 queue_head_id = excluded.queue_head_id,
1745 response_commit = excluded.response_commit,
1746 updated_at = excluded.updated_at
1747 "#,
1748 params![
1749 closeout.document_id,
1750 closeout.cycle_id,
1751 closeout.cycle_state,
1752 closeout.queue_head_id,
1753 closeout.response_commit,
1754 now
1755 ],
1756 )?;
1757
1758 for mutation in &closeout.mutations {
1759 tx.execute(
1760 r#"
1761 INSERT INTO pending_mutations (
1762 document_id,
1763 cycle_id,
1764 item_id,
1765 mutation_kind,
1766 status,
1767 recorded_at
1768 )
1769 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1770 ON CONFLICT(document_id, cycle_id, item_id, mutation_kind) DO UPDATE SET
1771 status = excluded.status,
1772 recorded_at = excluded.recorded_at
1773 "#,
1774 params![
1775 closeout.document_id,
1776 closeout.cycle_id,
1777 mutation.item_id,
1778 mutation.mutation_kind,
1779 mutation.status,
1780 now
1781 ],
1782 )?;
1783 }
1784
1785 tx.commit()?;
1786 Ok(())
1787}
1788
1789pub fn insert_admin_operation_in_db(
1790 conn: &Connection,
1791 operation_kind: &str,
1792 document_id: Option<&str>,
1793 status: &str,
1794 diagnostic_payload: Option<&str>,
1795) -> Result<u64> {
1796 conn.execute(
1797 r#"
1798 INSERT INTO admin_operations (
1799 operation_kind,
1800 document_id,
1801 status,
1802 diagnostic_payload,
1803 timestamp
1804 )
1805 VALUES (?1, ?2, ?3, ?4, ?5)
1806 "#,
1807 params![
1808 operation_kind,
1809 document_id,
1810 status,
1811 diagnostic_payload,
1812 sqlite_i64(timestamp_secs(), "admin operation timestamp")?
1813 ],
1814 )?;
1815 sqlite_u64(conn.last_insert_rowid(), "admin operation receipt id")
1816}
1817
1818pub fn insert_crash_recovery_marker_in_db(
1819 conn: &Connection,
1820 marker_kind: &str,
1821 document_id: Option<&str>,
1822 generation: Option<u64>,
1823 status: &str,
1824 diagnostic_payload: Option<&str>,
1825) -> Result<()> {
1826 conn.execute(
1827 r#"
1828 INSERT INTO crash_recovery_markers (
1829 marker_kind,
1830 document_id,
1831 generation,
1832 status,
1833 diagnostic_payload,
1834 timestamp
1835 )
1836 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1837 "#,
1838 params![
1839 marker_kind,
1840 document_id,
1841 generation
1842 .map(|value| sqlite_i64(value, "crash recovery marker generation"))
1843 .transpose()?,
1844 status,
1845 diagnostic_payload,
1846 sqlite_i64(timestamp_secs(), "crash recovery marker timestamp")?
1847 ],
1848 )?;
1849 Ok(())
1850}
1851
1852pub fn load_layout_state_from_db(conn: &Connection, scope: &str) -> Result<Vec<String>> {
1857 let raw: Option<String> = conn
1858 .query_row(
1859 "SELECT columns_json FROM layout_states WHERE scope = ?1",
1860 params![scope],
1861 |row| row.get(0),
1862 )
1863 .optional()?;
1864 match raw {
1865 Some(raw) => serde_json::from_str(&raw).context("failed to parse layout state from sqlite"),
1866 None => Ok(Vec::new()),
1867 }
1868}
1869
1870pub fn store_layout_state_in_db(conn: &Connection, scope: &str, columns: &[String]) -> Result<()> {
1871 let columns_json = serde_json::to_string(columns)?;
1872 conn.execute(
1873 r#"
1874 INSERT INTO layout_states (scope, columns_json, updated_at)
1875 VALUES (?1, ?2, ?3)
1876 ON CONFLICT(scope) DO UPDATE SET
1877 columns_json = excluded.columns_json,
1878 updated_at = excluded.updated_at
1879 "#,
1880 params![
1881 scope,
1882 columns_json,
1883 sqlite_i64(timestamp_secs(), "layout state timestamp")?
1884 ],
1885 )?;
1886 Ok(())
1887}
1888
1889pub fn layout_scope_exists(conn: &Connection, scope: &str) -> Result<bool> {
1890 let exists: Option<i64> = conn
1891 .query_row(
1892 "SELECT 1 FROM layout_states WHERE scope = ?1",
1893 params![scope],
1894 |row| row.get(0),
1895 )
1896 .optional()?;
1897 Ok(exists.is_some())
1898}
1899
1900#[cfg(test)]
1901mod tests {
1902 use super::*;
1903
1904 #[test]
1905 fn control_plane_store_counts_extended_fact_categories() -> Result<()> {
1906 let dir = tempfile::TempDir::new()?;
1907 let mut conn = open_state_db(dir.path())?;
1908 let record = ActorRecord {
1909 document_id: "tasks/control-plane.md".to_string(),
1910 session_id: "session-control-plane".to_string(),
1911 generation: 1,
1912 pane_id: "%1".to_string(),
1913 window_id: "@1".to_string(),
1914 harness: "codex".to_string(),
1915 state: ActorState::Ready,
1916 last_transition: ActorLastTransition {
1917 caller: "test".to_string(),
1918 reason: "store_actor_categories".to_string(),
1919 timestamp: timestamp_secs(),
1920 prior_generation: 0,
1921 new_generation: 1,
1922 },
1923 };
1924
1925 store_actor_record_tx(
1926 &mut conn,
1927 None,
1928 &record,
1929 Some("managed".to_string()),
1930 Some(7),
1931 )?;
1932 upsert_supervisor_lease_in_db(
1933 &conn,
1934 &record,
1935 Some(1234),
1936 Some("supervisor.sock"),
1937 "ready",
1938 )?;
1939 insert_dispatch_attempt_in_db(
1940 &conn,
1941 &DispatchAttemptInsert {
1942 document_id: &record.document_id,
1943 generation: record.generation,
1944 command_kind: "managed_reopen",
1945 accepted_stage: Some("accepted"),
1946 failed_stage: None,
1947 diagnostic_payload: "store actor count test",
1948 result_status: "accepted",
1949 proof_scope: "accepted_only",
1950 dispatch_start_proven: false,
1951 },
1952 )?;
1953 upsert_queue_head_in_db(
1954 &conn,
1955 &record.document_id,
1956 "agent:queue",
1957 Some("ctrlplane-storeactor"),
1958 "do [#ctrlplane-storeactor]",
1959 "selected",
1960 )?;
1961 let queue_head =
1962 load_queue_head_from_db(&conn, &record.document_id, "agent:queue")?.unwrap();
1963 assert_eq!(queue_head.generation, None);
1964 assert_eq!(queue_head.priority, 0);
1965 upsert_document_cycle_state_in_db(
1966 &conn,
1967 &record.document_id,
1968 "cycle-storeactor",
1969 "preflight_started",
1970 Some("ctrlplane-storeactor"),
1971 None,
1972 )?;
1973 commit_session_actor_closeout_in_db(
1974 &mut conn,
1975 &SessionActorCloseoutCommit {
1976 document_id: &record.document_id,
1977 cycle_id: "cycle-storeactor",
1978 cycle_state: "committed",
1979 queue_name: "agent:queue",
1980 queue_head_id: Some("ctrlplane-storeactor"),
1981 queue_head_prompt: Some("do [#ctrlplane-storeactor]"),
1982 queue_head_state: "consumed",
1983 response_commit: Some("commit-storeactor"),
1984 mutations: vec![SessionActorCloseoutMutation {
1985 item_id: "ctrlplane-storeactor",
1986 mutation_kind: "backlog_completion",
1987 status: "done",
1988 }],
1989 },
1990 )?;
1991 insert_projection_diagnostic(
1992 &conn,
1993 "session-actors.json",
1994 &record.document_id,
1995 "projection lag",
1996 )?;
1997 insert_admin_operation_in_db(
1998 &conn,
1999 "projection_repair",
2000 Some(&record.document_id),
2001 "accepted",
2002 Some("store actor count test"),
2003 )?;
2004 let queue_control = upsert_queue_control_in_db(
2005 &conn,
2006 &QueueControlInsert {
2007 scope_kind: "document",
2008 scope_id: &record.document_id,
2009 state: "paused",
2010 reason: Some("store actor count test"),
2011 operation_receipt_id: Some(1),
2012 },
2013 )?;
2014 insert_queue_backpressure_in_db(
2015 &conn,
2016 &QueueBackpressureInsert {
2017 document_id: &record.document_id,
2018 generation: Some(record.generation),
2019 command_kind: "managed_reopen",
2020 capacity_class: "queue_paused",
2021 reason: "store actor count test",
2022 dispatch_receipt_id: Some(queue_control.receipt_id),
2023 },
2024 )?;
2025 insert_crash_recovery_marker_in_db(
2026 &conn,
2027 "startup_reconcile",
2028 Some(&record.document_id),
2029 Some(record.generation),
2030 "pending",
2031 Some("store actor count test"),
2032 )?;
2033 store_layout_state_in_db(&conn, "default", &["%1".to_string()])?;
2034
2035 let counts = load_control_plane_store_counts(&conn)?;
2036 assert_eq!(counts.actor_documents, 1);
2037 assert_eq!(counts.live_actor_documents, 1);
2038 assert_eq!(counts.actor_transitions, 1);
2039 assert_eq!(counts.supervisor_leases, 1);
2040 assert_eq!(counts.dispatch_receipts, 1);
2041 assert_eq!(counts.queue_heads, 1);
2042 assert_eq!(counts.document_cycles, 1);
2043 assert_eq!(counts.pending_mutations, 1);
2044 assert_eq!(counts.projection_diagnostics, 1);
2045 assert_eq!(counts.admin_operations, 1);
2046 assert_eq!(counts.queue_controls, 1);
2047 assert_eq!(counts.queue_backpressure, 1);
2048 assert_eq!(counts.crash_recovery_markers, 1);
2049 assert_eq!(counts.layout_states, 1);
2050 assert_eq!(counts.total_authoritative_rows(), 13);
2051
2052 Ok(())
2053 }
2054}