Skip to main content

agent_doc_sqlite/
state_store.rs

1//! Project-controller SQLite state store.
2//!
3//! This module owns every `rusqlite::Connection` interaction for the project
4//! controller's durable actor/lease/dispatch/diagnostic/layout state, plus the
5//! storage and status types those queries read and write. It deliberately
6//! depends only on `rusqlite`, `serde_json`, and `std` so that
7//! `agent-doc-orchestration` can call into it without pulling the bundled
8//! SQLite C build into its own compile graph.
9//!
10//! Orchestration glue (ops-log, `sessions.json`/`session-actors.json`
11//! projection, `read_bootstrap`, drift verification) stays in
12//! `agent-doc-orchestration`; this module exposes the SQL primitives those
13//! callers stitch together.
14
15use 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// ---------------------------------------------------------------------------
26// Storage types (moved from agent-doc-orchestration::session_actor).
27// ---------------------------------------------------------------------------
28
29#[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// ---------------------------------------------------------------------------
87// Status types (moved from agent-doc-orchestration::project_controller).
88// ---------------------------------------------------------------------------
89
90#[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
311// ---------------------------------------------------------------------------
312// Connection + schema.
313// ---------------------------------------------------------------------------
314
315pub 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
561// ---------------------------------------------------------------------------
562// Conversion + time helpers.
563// ---------------------------------------------------------------------------
564
565pub 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
651// ---------------------------------------------------------------------------
652// Actor reads.
653// ---------------------------------------------------------------------------
654
655pub 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
1144// ---------------------------------------------------------------------------
1145// State-event ledger reads.
1146// ---------------------------------------------------------------------------
1147
1148fn 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
1196// ---------------------------------------------------------------------------
1197// Actor writes.
1198// ---------------------------------------------------------------------------
1199
1200pub 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
1237/// Upsert the authoritative `documents` row for an actor.
1238///
1239/// `launch_mode` and `controller_epoch` are the lifted orchestration tendril:
1240/// the orchestration caller computes them from `read_bootstrap` (launch mode
1241/// short string + bootstrap epoch) and passes them in. Pass `None`/`None` when
1242/// no bootstrap state is available.
1243pub 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
1356/// `#qflood`: is a dispatch already in flight (accepted, not yet consumed) for this
1357/// document at `generation`? Mirrors the open-dispatch shape the restart reconciler
1358/// keys on. An open dispatch means the current turn already has work queued/running,
1359/// so an auto re-fire would only pile a redundant trigger into the busy pane.
1360pub 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
1381/// `#ctlrecycle`: is ANY dispatch in flight across every document/generation? The
1382/// controller's idle self-recycle (R1) uses this as its idle proof — it must never
1383/// exit while a turn is mid-dispatch for any session it coordinates. Same open-set
1384/// definition as [`has_open_in_flight_dispatch`] without the document/generation
1385/// filter.
1386pub 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
1401/// `#qflood`: mark every open in-flight dispatch for this document consumed. Called
1402/// when the actor transitions to `Ready` (the turn finished → its dispatch is done),
1403/// keeping the open-dispatch set accurate for the next busy episode's coalescing and
1404/// for restart recovery. Returns the number of receipts released.
1405pub 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
1554/// Run the compare-and-swap actor write transaction.
1555///
1556/// Mirrors the prior `project_controller::store_actor_record` transaction body:
1557/// load the previous record, enforce the optional CAS expectation, reject
1558/// generation regressions, recover cross-document pane aliases, insert the
1559/// transition, then upsert the document row using the lifted
1560/// `launch_mode`/`controller_epoch` bootstrap tendril.
1561pub 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
1611/// `#actorprune`: hard-delete a dead actor record and its history/lease rows.
1612///
1613/// `close_stale_starting_actors` only TRANSITIONS `Starting` actors to `Closed`;
1614/// nothing removes long-dead `Closed` rows, so the `documents` projection (and
1615/// `admin list`) grows without bound. This removes the `documents` row plus its
1616/// `actor_transitions` history and `supervisor_leases` rows for one document_id
1617/// in a single transaction. Returns the number of `documents` rows removed (0 or
1618/// 1).
1619pub 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
1637/// True when the `documents` table holds no rows yet.
1638///
1639/// Orchestration uses this to gate the legacy `session-actors.json` read so the
1640/// JSON is only loaded when a migration could actually run.
1641pub 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
1646/// Migrate a legacy `session-actors.json` store into the empty sqlite state.
1647///
1648/// The orchestration caller loads the legacy JSON and the bootstrap-derived
1649/// `launch_mode`/`controller_epoch` tendril, then hands them to this routine,
1650/// which only runs when the `documents` table is still empty.
1651pub 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
1676// ---------------------------------------------------------------------------
1677// Control-plane durable facts.
1678// ---------------------------------------------------------------------------
1679
1680pub 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
1991// ---------------------------------------------------------------------------
1992// Layout state.
1993// ---------------------------------------------------------------------------
1994
1995pub 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}