diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f4797aa4fd..ed5d11fa8c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -107,7 +107,9 @@ tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7", default-features = false } petgraph = { version = "0.6.4", default-features = false, features = ["serde-1"] } chrono = { version = "0.4", features = ["serde"] } -rusqlite = { version = "0.32", features = ["bundled"] } +# `trace` powers the statement-count assertions in the Agent Org schema +# coordinator tests (no-op boots must execute a constant statement count). +rusqlite = { version = "0.32", features = ["bundled", "trace"] } axum = { version = "0.8", features = ["ws"] } # Modern Objective-C bindings (replaces the unmaintained `objc 0.2` / diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/message.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/message.rs index 67bd3540c3..d3c7377e4e 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/message.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/message.rs @@ -683,7 +683,7 @@ mod tests { fn seed_minimal_running_run_for_delivery_resolution(run_id: &str) { let conn = get_connection().expect("open sandbox database"); conn.execute_batch( - "CREATE TABLE IF NOT EXISTS agent_org_runs ( + "CREATE TABLE IF NOT EXISTS agent_org_runtime_runs ( id TEXT PRIMARY KEY, status TEXT NOT NULL, org_snapshot_json TEXT, @@ -705,7 +705,7 @@ mod tests { org_member_id TEXT, updated_at TEXT NOT NULL ); - CREATE TABLE IF NOT EXISTS agent_org_tasks ( + CREATE TABLE IF NOT EXISTS agent_org_runtime_tasks ( id TEXT PRIMARY KEY, org_run_id TEXT NOT NULL );", @@ -721,7 +721,7 @@ mod tests { ) .expect("seed coordinator session"); conn.execute( - "INSERT INTO agent_org_runs (id, status, org_snapshot_json, root_session_id) + "INSERT INTO agent_org_runtime_runs (id, status, org_snapshot_json, root_session_id) VALUES (?1, 'running', NULL, ?2)", params![run_id, &root_session_id], ) @@ -736,7 +736,7 @@ mod tests { let payload_json = serde_json::to_string(&message).expect("serialize legacy payload"); let conn = get_connection().expect("open sandbox database"); conn.execute( - "INSERT INTO agent_inbox ( + "INSERT INTO agent_org_runtime_inbox ( recipient_agent_id, recipient_member_id, sender_agent_id, sender_member_id, org_run_id, payload_kind, payload_json, request_id, @@ -1384,7 +1384,7 @@ mod tests { ); let conn = get_connection().expect("open sandbox database"); conn.execute( - "INSERT INTO agent_inbox_materializations ( + "INSERT INTO agent_org_runtime_inbox_materializations ( inbox_id, session_id, transcript_message_id, transcript_intent_id, materialized_at ) VALUES (?1, 'old-session', 'message-1', 'intent-1', ?2)", @@ -1457,7 +1457,7 @@ mod tests { let source = seed_legacy_orphan_inbox_row(run_id, "Original", "Original work"); let conn = get_connection().expect("open sandbox database"); conn.execute( - "INSERT INTO agent_org_tasks (id, org_run_id) VALUES ('replacement-task', ?1)", + "INSERT INTO agent_org_runtime_tasks (id, org_run_id) VALUES ('replacement-task', ?1)", params![run_id], ) .expect("seed replacement task"); @@ -1532,7 +1532,7 @@ mod tests { }; let conn = get_connection().expect("open sandbox database"); conn.execute( - "UPDATE agent_org_runs SET org_snapshot_json=?1 WHERE id=?2", + "UPDATE agent_org_runtime_runs SET org_snapshot_json=?1 WHERE id=?2", params![ serde_json::to_string(&crate::definitions::orgs::AgentOrgLaunchSnapshot::from( &org diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/mod.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/mod.rs index dd17cedeb7..b806fd5382 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/mod.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/mod.rs @@ -51,6 +51,7 @@ pub use record::{ InsertInboxParams, ResolveInboxDeliveryError, ResolveInboxDeliveryParams, }; pub use schema::init_schema; +pub(crate) use schema::{create_schema, repair_dangling_materializations}; /// Reserved sender id for system-generated agent inbox rows. /// diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/schema.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/schema.rs index 5a0a56c599..e3c8d81ee8 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/schema.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/schema.rs @@ -1,10 +1,14 @@ -//! `agent_inbox` table DDL, column back-fills, and receipt self-heal. +//! `agent_org_runtime_inbox` table DDL and receipt self-heal. use rusqlite::{Connection, Result as SqliteResult}; use crate::coordination::agent_org_payload_limits as limits; -/// Initialize the `agent_inbox` table. +/// Initialize the `agent_org_runtime_inbox` table. +/// +/// Tests-only convenience: production initialization goes through the +/// namespace coordinator (`coordination::schema::initialize`), never this +/// module-level entry point. /// /// Hot-path indexes: /// - `(recipient_member_id, read_at, created_at)` — materialized org member drain query. @@ -12,18 +16,21 @@ use crate::coordination::agent_org_payload_limits as limits; /// - `(org_run_id, created_at)` — bounded debug / E2E history pages. /// - `(request_id)` — RPC correlation lookups. pub fn init_schema(conn: &Connection) -> SqliteResult<()> { + create_schema(conn)?; + repair_dangling_materializations(conn) +} + +pub(crate) fn create_schema(conn: &Connection) -> SqliteResult<()> { create_agent_inbox_table(conn)?; - ensure_agent_inbox_column(conn, "causation_inbox_id", "INTEGER")?; - ensure_agent_inbox_column(conn, "display_text", "TEXT")?; let schema = format!( - "CREATE TABLE IF NOT EXISTS agent_inbox_materializations ( + "CREATE TABLE IF NOT EXISTS agent_org_runtime_inbox_materializations ( inbox_id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, transcript_message_id TEXT NOT NULL, transcript_intent_id TEXT NOT NULL, materialized_at TEXT NOT NULL ); - CREATE TABLE IF NOT EXISTS agent_inbox_delivery_resolutions ( + CREATE TABLE IF NOT EXISTS agent_org_runtime_inbox_delivery_resolutions ( inbox_id INTEGER PRIMARY KEY, org_run_id TEXT NOT NULL, resolution_kind TEXT NOT NULL @@ -43,25 +50,25 @@ pub fn init_schema(conn: &Connection) -> SqliteResult<()> { <> (replacement_task_id IS NOT NULL))) ) ); - CREATE INDEX IF NOT EXISTS idx_agent_inbox_delivery_resolutions_run - ON agent_inbox_delivery_resolutions(org_run_id, inbox_id); - CREATE INDEX IF NOT EXISTS idx_agent_inbox_materializations_session - ON agent_inbox_materializations(session_id, inbox_id); - CREATE INDEX IF NOT EXISTS idx_agent_inbox_recipient_member_unread - ON agent_inbox(recipient_member_id, read_at, created_at); - CREATE INDEX IF NOT EXISTS idx_agent_inbox_recipient_unread - ON agent_inbox(recipient_agent_id, read_at, created_at); - CREATE INDEX IF NOT EXISTS idx_agent_inbox_org_run - ON agent_inbox(org_run_id, created_at); - CREATE INDEX IF NOT EXISTS idx_agent_inbox_org_run_id - ON agent_inbox(org_run_id, id); - CREATE INDEX IF NOT EXISTS idx_agent_inbox_run_unread_recipient - ON agent_inbox(org_run_id, recipient_member_id, recipient_agent_id, id) + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_delivery_resolutions_run + ON agent_org_runtime_inbox_delivery_resolutions(org_run_id, inbox_id); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_materializations_session + ON agent_org_runtime_inbox_materializations(session_id, inbox_id); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_recipient_member_unread + ON agent_org_runtime_inbox(recipient_member_id, read_at, created_at); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_recipient_unread + ON agent_org_runtime_inbox(recipient_agent_id, read_at, created_at); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_org_run + ON agent_org_runtime_inbox(org_run_id, created_at); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_org_run_id + ON agent_org_runtime_inbox(org_run_id, id); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_run_unread_recipient + ON agent_org_runtime_inbox(org_run_id, recipient_member_id, recipient_agent_id, id) WHERE read_at IS NULL; - CREATE INDEX IF NOT EXISTS idx_agent_inbox_run_kind_id - ON agent_inbox(org_run_id, payload_kind, id); - CREATE INDEX IF NOT EXISTS idx_agent_inbox_run_task_assignment_v4 - ON agent_inbox( + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_run_kind_id + ON agent_org_runtime_inbox(org_run_id, payload_kind, id); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_run_task_assignment_v4 + ON agent_org_runtime_inbox( org_run_id, recipient_member_id, json_extract( @@ -80,13 +87,10 @@ pub fn init_schema(conn: &Connection) -> SqliteResult<()> { THEN payload_json ELSE '{{}}' END, '$.task_id' )='text'; - DROP INDEX IF EXISTS idx_agent_inbox_run_task_assignment_v3; - DROP INDEX IF EXISTS idx_agent_inbox_run_task_assignment_v2; - CREATE INDEX IF NOT EXISTS idx_agent_inbox_request_id - ON agent_inbox(request_id); - DROP INDEX IF EXISTS idx_agent_inbox_causation_once; - CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_inbox_causation_recipient_once - ON agent_inbox( + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_request_id + ON agent_org_runtime_inbox(request_id); + CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_causation_recipient_once + ON agent_org_runtime_inbox( causation_inbox_id, payload_kind, recipient_agent_id, @@ -95,7 +99,10 @@ pub fn init_schema(conn: &Connection) -> SqliteResult<()> { WHERE causation_inbox_id IS NOT NULL;", payload_max = limits::AGENT_INBOX_PAYLOAD_MAX_BYTES, ); - conn.execute_batch(&schema)?; + conn.execute_batch(&schema) +} + +pub(crate) fn repair_dangling_materializations(conn: &Connection) -> SqliteResult<()> { // Self-heal only provably dangling receipts. Source Inbox rows remain // unread, allowing a healthy replacement Session to materialize them. let transcript_tables_exist: bool = conn.query_row( @@ -106,13 +113,13 @@ pub fn init_schema(conn: &Connection) -> SqliteResult<()> { )?; if transcript_tables_exist { conn.execute( - "DELETE FROM agent_inbox_materializations AS receipt + "DELETE FROM agent_org_runtime_inbox_materializations AS receipt WHERE NOT EXISTS ( - SELECT 1 FROM agent_inbox inbox + SELECT 1 FROM agent_org_runtime_inbox inbox WHERE inbox.id=receipt.inbox_id AND inbox.read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution WHERE resolution.inbox_id=inbox.id ) ) @@ -131,28 +138,9 @@ pub fn init_schema(conn: &Connection) -> SqliteResult<()> { Ok(()) } -fn ensure_agent_inbox_column( - conn: &Connection, - column_name: &str, - column_definition: &str, -) -> SqliteResult<()> { - let mut stmt = conn.prepare("PRAGMA table_info(agent_inbox)")?; - let columns = stmt.query_map([], |row| row.get::<_, String>(1))?; - for column in columns { - if column? == column_name { - return Ok(()); - } - } - conn.execute( - &format!("ALTER TABLE agent_inbox ADD COLUMN {column_name} {column_definition}"), - [], - )?; - Ok(()) -} - fn create_agent_inbox_table(conn: &Connection) -> SqliteResult<()> { conn.execute_batch( - "CREATE TABLE IF NOT EXISTS agent_inbox ( + "CREATE TABLE IF NOT EXISTS agent_org_runtime_inbox ( id INTEGER PRIMARY KEY AUTOINCREMENT, recipient_agent_id TEXT NOT NULL, recipient_member_id TEXT, @@ -175,29 +163,12 @@ mod tests { use super::*; #[test] - fn init_schema_adds_group_chat_display_text_to_legacy_inbox() { + fn canonical_schema_contains_current_columns_and_indexes() { let conn = rusqlite::Connection::open_in_memory().expect("open in-memory database"); - conn.execute_batch( - "CREATE TABLE agent_inbox ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - recipient_agent_id TEXT NOT NULL, - recipient_member_id TEXT, - sender_agent_id TEXT NOT NULL, - sender_member_id TEXT, - org_run_id TEXT, - payload_kind TEXT NOT NULL, - payload_json TEXT NOT NULL, - request_id TEXT, - created_at TEXT NOT NULL, - read_at TEXT - );", - ) - .expect("create legacy inbox table"); - - init_schema(&conn).expect("upgrade legacy inbox schema"); + init_schema(&conn).expect("create canonical inbox schema"); let mut stmt = conn - .prepare("PRAGMA table_info(agent_inbox)") + .prepare("PRAGMA table_info(agent_org_runtime_inbox)") .expect("inspect inbox schema"); let columns = stmt .query_map([], |row| row.get::<_, String>(1)) @@ -206,5 +177,18 @@ mod tests { .expect("collect inbox columns"); assert!(columns.iter().any(|column| column == "causation_inbox_id")); assert!(columns.iter().any(|column| column == "display_text")); + let required_index_count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM sqlite_master + WHERE type='index' AND name IN ( + 'idx_agent_org_runtime_inbox_run_unread_recipient', + 'idx_agent_org_runtime_inbox_run_task_assignment_v4', + 'idx_agent_org_runtime_inbox_causation_recipient_once' + )", + [], + |row| row.get(0), + ) + .expect("inspect canonical inbox indexes"); + assert_eq!(required_index_count, 3); } } diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/store_drain.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/store_drain.rs index 2f102d7bcf..9f7ba60fd9 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/store_drain.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/store_drain.rs @@ -6,7 +6,9 @@ use std::collections::HashSet; use rusqlite::{params, OptionalExtension}; -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use crate::coordination::agent_org_payload_limits as limits; @@ -23,16 +25,16 @@ impl AgentInboxStore { recipient_member_id: &str, org_run_id: &str, ) -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; conn.query_row( "SELECT EXISTS( - SELECT 1 FROM agent_inbox + SELECT 1 FROM agent_org_runtime_inbox WHERE recipient_member_id = ?1 AND org_run_id = ?2 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id ) )", params![recipient_member_id, org_run_id], @@ -46,7 +48,7 @@ impl AgentInboxStore { recipient_member_id: &str, org_run_id: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( "SELECT id, @@ -60,13 +62,13 @@ impl AgentInboxStore { request_id, created_at, read_at - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE recipient_member_id = ?1 AND org_run_id = ?2 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id ) ORDER BY id ASC", ) @@ -88,15 +90,15 @@ impl AgentInboxStore { recipient_member_id: &str, org_run_id: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; conn.query_row( - "SELECT MAX(id) FROM agent_inbox + "SELECT MAX(id) FROM agent_org_runtime_inbox WHERE recipient_member_id=?1 AND org_run_id=?2 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id )", params![recipient_member_id, org_run_id], |row| row.get(0), @@ -112,17 +114,17 @@ impl AgentInboxStore { org_run_id: &str, boundary_id: i64, ) -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let count: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_inbox + "SELECT COUNT(*) FROM agent_org_runtime_inbox WHERE recipient_member_id=?1 AND org_run_id=?2 AND id<=?3 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id )", params![recipient_member_id, org_run_id, boundary_id], |row| row.get(0), @@ -139,7 +141,7 @@ impl AgentInboxStore { recipient_member_id: &str, org_run_id: &str, ) -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( "SELECT id, @@ -165,13 +167,13 @@ impl AgentInboxStore { request_id, created_at, read_at - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE recipient_member_id = ?1 AND org_run_id = ?2 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id ) ORDER BY id ASC LIMIT ?3", @@ -239,7 +241,7 @@ impl AgentInboxStore { } let (updated, changed_run_ids) = with_sessions_writer( || -> Result<(usize, HashSet), String> { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -255,16 +257,16 @@ impl AgentInboxStore { "SELECT read_at, EXISTS( SELECT 1 - FROM agent_inbox_materializations receipt - WHERE receipt.inbox_id=agent_inbox.id + FROM agent_org_runtime_inbox_materializations receipt + WHERE receipt.inbox_id=agent_org_runtime_inbox.id AND receipt.session_id=?2 ), EXISTS( SELECT 1 - FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id ) - FROM agent_inbox WHERE id=?1", + FROM agent_org_runtime_inbox WHERE id=?1", ) .map_err(|err| err.to_string())?; for id in ids { @@ -282,16 +284,16 @@ impl AgentInboxStore { } let mut stmt = tx .prepare( - "UPDATE agent_inbox + "UPDATE agent_org_runtime_inbox SET read_at=?1 WHERE id=?2 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id ) AND EXISTS ( - SELECT 1 FROM agent_inbox_materializations receipt - WHERE receipt.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_materializations receipt + WHERE receipt.inbox_id=agent_org_runtime_inbox.id AND receipt.session_id=?3 )", ) @@ -299,7 +301,7 @@ impl AgentInboxStore { for id in ids { let org_run_id = tx .query_row( - "SELECT org_run_id FROM agent_inbox WHERE id=?1 AND read_at IS NULL", + "SELECT org_run_id FROM agent_org_runtime_inbox WHERE id=?1 AND read_at IS NULL", params![id], |row| row.get::<_, Option>(0), ) @@ -319,19 +321,19 @@ impl AgentInboxStore { } else { let mut stmt = tx .prepare( - "UPDATE agent_inbox + "UPDATE agent_org_runtime_inbox SET read_at=?1 WHERE id=?2 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id )", ) .map_err(|err| err.to_string())?; for id in ids { let org_run_id = tx .query_row( - "SELECT org_run_id FROM agent_inbox WHERE id=?1 AND read_at IS NULL", + "SELECT org_run_id FROM agent_org_runtime_inbox WHERE id=?1 AND read_at IS NULL", params![id], |row| row.get::<_, Option>(0), ) @@ -354,7 +356,7 @@ impl AgentInboxStore { if let Some(session_id) = materialization_session_id { let mut stmt = tx .prepare( - "DELETE FROM agent_inbox_materializations + "DELETE FROM agent_org_runtime_inbox_materializations WHERE inbox_id=?1 AND session_id=?2", ) .map_err(|err| err.to_string())?; @@ -364,7 +366,7 @@ impl AgentInboxStore { } } else { let mut stmt = tx - .prepare("DELETE FROM agent_inbox_materializations WHERE inbox_id=?1") + .prepare("DELETE FROM agent_org_runtime_inbox_materializations WHERE inbox_id=?1") .map_err(|err| err.to_string())?; for id in ids { stmt.execute(params![id]).map_err(|err| err.to_string())?; diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/store_read.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/store_read.rs index 7bb2fc3bdd..f022a8d624 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/store_read.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/store_read.rs @@ -6,7 +6,9 @@ use rusqlite::{params, Connection, OptionalExtension}; use std::collections::HashSet; use crate::coordination::agent_org_payload_limits as limits; -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use super::message::AgentMessage; #[cfg(test)] @@ -25,12 +27,12 @@ pub(super) const UNREAD_COUNTS_BY_RECIPIENT_SQL: &str = "SELECT recipient_agent_ recipient_member_id, COUNT(*) AS unread_count, MAX(id) AS max_unread_id - FROM agent_inbox INDEXED BY idx_agent_inbox_run_unread_recipient + FROM agent_org_runtime_inbox INDEXED BY idx_agent_org_runtime_inbox_run_unread_recipient WHERE org_run_id = ?1 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id ) GROUP BY recipient_member_id, recipient_agent_id ORDER BY recipient_member_id ASC, recipient_agent_id ASC"; @@ -39,7 +41,7 @@ pub(super) fn task_assignment_lookup_sql() -> String { let payload_max = limits::AGENT_INBOX_PAYLOAD_MAX_BYTES; format!( "SELECT payload_json - FROM agent_inbox INDEXED BY idx_agent_inbox_run_task_assignment_v4 + FROM agent_org_runtime_inbox INDEXED BY idx_agent_org_runtime_inbox_run_task_assignment_v4 WHERE org_run_id=?1 AND recipient_member_id=?2 AND payload_kind='task_assigned' @@ -121,7 +123,7 @@ fn load_delivery_resolution( "SELECT inbox_id, org_run_id, resolution_kind, resolved_by_member_id, reason, replacement_inbox_id, replacement_task_id, created_at - FROM agent_inbox_delivery_resolutions + FROM agent_org_runtime_inbox_delivery_resolutions WHERE inbox_id=?1 AND org_run_id=?2 LIMIT 1", params![inbox_id, org_run_id], @@ -171,7 +173,7 @@ impl AgentInboxStore { /// tiny number of rows. Production and debug paths use bounded pages. #[cfg(test)] pub fn list_by_run(org_run_id: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( "SELECT id, @@ -185,7 +187,7 @@ impl AgentInboxStore { request_id, created_at, read_at - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE org_run_id = ?1 ORDER BY id ASC", ) @@ -209,7 +211,7 @@ impl AgentInboxStore { limit: usize, ) -> Result { let bounded_limit = limit.clamp(1, MAX_INBOX_HISTORY_PAGE_ROWS); - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( "SELECT id, @@ -245,7 +247,7 @@ impl AgentInboxStore { CASE WHEN request_id IS NULL THEN NULL ELSE substr(request_id,1,1000) END, substr(created_at,1,64), CASE WHEN read_at IS NULL THEN NULL ELSE substr(read_at,1,64) END - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE org_run_id=?1 AND id>?2 ORDER BY id ASC LIMIT ?3", @@ -300,10 +302,10 @@ impl AgentInboxStore { } pub fn count_by_run(org_run_id: &str) -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let count: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_inbox WHERE org_run_id=?1", + "SELECT COUNT(*) FROM agent_org_runtime_inbox WHERE org_run_id=?1", params![org_run_id], |row| row.get(0), ) @@ -322,7 +324,7 @@ impl AgentInboxStore { if inbox_id <= 0 { return Err("inbox_id must be a positive integer".to_string()); } - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; conn.query_row( "SELECT id, CASE WHEN length(CAST(recipient_agent_id AS BLOB))<=?3 @@ -357,7 +359,7 @@ impl AgentInboxStore { CASE WHEN request_id IS NULL THEN NULL ELSE substr(request_id,1,1000) END, substr(created_at,1,64), CASE WHEN read_at IS NULL THEN NULL ELSE substr(read_at,1,64) END - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE org_run_id=?1 AND id=?2 LIMIT 1", params![ @@ -376,7 +378,7 @@ impl AgentInboxStore { org_run_id: &str, inbox_id: i64, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; load_delivery_resolution(&conn, org_run_id, inbox_id) } @@ -448,7 +450,7 @@ impl AgentInboxStore { with_sessions_writer( || -> Result { - let mut conn = get_connection().map_err(|err| storage(err.to_string()))?; + let mut conn = runtime_connection().map_err(|err| storage(err.to_string()))?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| storage(err.to_string()))?; @@ -465,7 +467,7 @@ impl AgentInboxStore { let source: Option<(Option, Option)> = tx .query_row( "SELECT recipient_member_id, read_at - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE id=?1 AND org_run_id=?2 LIMIT 1", params![params.inbox_id, ¶ms.org_run_id], @@ -531,10 +533,10 @@ impl AgentInboxStore { inbox.read_at, EXISTS( SELECT 1 - FROM agent_inbox_delivery_resolutions resolution + FROM agent_org_runtime_inbox_delivery_resolutions resolution WHERE resolution.inbox_id=inbox.id ) - FROM agent_inbox inbox + FROM agent_org_runtime_inbox inbox WHERE inbox.id=?1 AND inbox.org_run_id=?2 LIMIT 1", params![replacement_inbox_id, ¶ms.org_run_id], @@ -570,7 +572,7 @@ impl AgentInboxStore { let replacement_exists: bool = tx .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_tasks + SELECT 1 FROM agent_org_runtime_tasks WHERE id=?1 AND org_run_id=?2 )", params![replacement_task_id, ¶ms.org_run_id], @@ -587,7 +589,7 @@ impl AgentInboxStore { let created_at = chrono::Utc::now().to_rfc3339(); tx.execute( - "INSERT INTO agent_inbox_delivery_resolutions ( + "INSERT INTO agent_org_runtime_inbox_delivery_resolutions ( inbox_id, org_run_id, resolution_kind, resolved_by_member_id, reason, replacement_inbox_id, replacement_task_id, created_at @@ -608,7 +610,7 @@ impl AgentInboxStore { // not later acknowledge it as delivered. The guarded mark-read // path also rechecks the resolution table. tx.execute( - "DELETE FROM agent_inbox_materializations WHERE inbox_id=?1", + "DELETE FROM agent_org_runtime_inbox_materializations WHERE inbox_id=?1", params![params.inbox_id], ) .map_err(|err| storage(err.to_string()))?; @@ -639,7 +641,7 @@ impl AgentInboxStore { return Ok(Vec::new()); } let bounded_limit = limit.min(MAX_RUN_INBOX_SNAPSHOT_ROWS) as i64; - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( "SELECT id, @@ -677,7 +679,7 @@ impl AgentInboxStore { request_id, created_at, read_at - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE org_run_id = ?1 ORDER BY id DESC LIMIT ?2 @@ -705,7 +707,7 @@ impl AgentInboxStore { org_run_id: &str, limit: usize, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::list_recent_previews_by_run_with_connection(&conn, org_run_id, limit) } @@ -788,11 +790,11 @@ impl AgentInboxStore { ELSE NULL END AS display_preview, ( SELECT resolution.resolution_kind - FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id LIMIT 1 ) AS delivery_resolution - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE org_run_id = ?1 ORDER BY id DESC LIMIT ?2 @@ -823,7 +825,7 @@ impl AgentInboxStore { pub fn run_counts_by_recipient( org_run_id: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::run_counts_by_recipient_with_connection(&conn, org_run_id) } @@ -841,11 +843,11 @@ impl AgentInboxStore { SUM(CASE WHEN read_at IS NULL AND NOT EXISTS ( SELECT 1 - FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id ) THEN 1 ELSE 0 END) AS unread_count - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE org_run_id = ?1 GROUP BY recipient_member_id, recipient_agent_id ORDER BY recipient_member_id ASC, recipient_agent_id ASC", @@ -902,11 +904,11 @@ impl AgentInboxStore { /// inbox history in Rust. #[cfg(test)] pub(super) fn task_assignment_ids_by_run(org_run_id: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( "SELECT DISTINCT json_extract(payload_json, '$.task_id') - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE org_run_id=?1 AND payload_kind='task_assigned' AND json_valid(payload_json) @@ -940,7 +942,7 @@ impl AgentInboxStore { let mut task_stmt = conn .prepare( "SELECT id, owner - FROM agent_org_tasks + FROM agent_org_runtime_tasks WHERE org_run_id=?1 AND status IN ('pending','in_progress') AND owner IS NOT NULL @@ -1003,7 +1005,7 @@ impl AgentInboxStore { recipient_member_id: &str, org_run_id: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::unread_fingerprint_for_member_with_connection(&conn, recipient_member_id, org_run_id) } @@ -1018,13 +1020,13 @@ impl AgentInboxStore { let (max_id, count): (Option, i64) = conn .query_row( "SELECT MAX(id), COUNT(*) - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE recipient_member_id=?1 AND org_run_id=?2 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id )", params![recipient_member_id, org_run_id], |row| Ok((row.get(0)?, row.get(1)?)), diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/store_write.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/store_write.rs index bef2d4a65c..1d17c82ec7 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/store_write.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/store_write.rs @@ -4,7 +4,9 @@ use rusqlite::{params, Connection}; use crate::coordination::agent_org_payload_limits as limits; -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use super::record::row_to_record; use super::{AgentInboxRecord, AgentInboxStore, InsertInboxParams}; @@ -17,7 +19,7 @@ impl AgentInboxStore { pub fn insert(params: InsertInboxParams) -> Result { let changed_org_run_id = params.org_run_id.clone(); let record = with_sessions_writer(|| -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::insert_in_tx(&conn, params) })?; if let Some(org_run_id) = changed_org_run_id { @@ -39,14 +41,14 @@ impl AgentInboxStore { .ok_or_else(|| "insert_if_run_running requires org_run_id".to_string())? .to_string(); with_sessions_writer(|| -> Result, String> { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; let running: bool = tx .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_runs WHERE id=?1 AND status='running' + SELECT 1 FROM agent_org_runtime_runs WHERE id=?1 AND status='running' )", params![&run_id], |row| row.get(0), @@ -76,7 +78,7 @@ impl AgentInboxStore { return Err("causation_inbox_id must be a positive inbox row id".into()); } with_sessions_writer(|| -> Result<(AgentInboxRecord, bool), String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::insert_in_tx_with_causation(&conn, params, Some(causation_inbox_id)) }) } @@ -146,7 +148,7 @@ impl AgentInboxStore { let now = chrono::Utc::now().to_rfc3339(); let insert_sql = if causation_inbox_id.is_some() { - "INSERT OR IGNORE INTO agent_inbox ( + "INSERT OR IGNORE INTO agent_org_runtime_inbox ( recipient_agent_id, recipient_member_id, sender_agent_id, @@ -160,7 +162,7 @@ impl AgentInboxStore { causation_inbox_id ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, NULL, ?10)" } else { - "INSERT INTO agent_inbox ( + "INSERT INTO agent_org_runtime_inbox ( recipient_agent_id, recipient_member_id, sender_agent_id, @@ -209,7 +211,7 @@ impl AgentInboxStore { request_id, created_at, read_at - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE causation_inbox_id = ?1 AND payload_kind = ?2 AND recipient_agent_id = ?3 diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/tests.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/tests.rs index 44e9b1d6b1..47aad26e12 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/tests.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_inbox/tests.rs @@ -84,7 +84,7 @@ fn inbox_history_pages_are_cursor_bounded_without_gaps() { let now = chrono::Utc::now().to_rfc3339(); for _ in 0..205 { tx.execute( - "INSERT INTO agent_inbox ( + "INSERT INTO agent_org_runtime_inbox ( recipient_agent_id, recipient_member_id, sender_agent_id, sender_member_id, org_run_id, payload_kind, payload_json, request_id, created_at, read_at, causation_inbox_id @@ -145,7 +145,7 @@ fn recent_run_snapshot_is_bounded_and_counts_do_not_load_payloads() { // write boundary no longer permits creating new ones. let conn = get_connection().expect("open inbox database for legacy fixture"); conn.execute( - "INSERT INTO agent_inbox ( + "INSERT INTO agent_org_runtime_inbox ( recipient_agent_id, recipient_member_id, sender_agent_id, sender_member_id, org_run_id, payload_kind, payload_json, request_id, created_at, read_at, causation_inbox_id @@ -217,7 +217,7 @@ fn recent_run_snapshot_is_bounded_and_counts_do_not_load_payloads() { assert!( details .iter() - .any(|detail| detail.contains("idx_agent_inbox_run_unread_recipient")), + .any(|detail| detail.contains("idx_agent_org_runtime_inbox_run_unread_recipient")), "watchdog/run-view unread aggregation must stay on the partial unread index: {details:?}" ); assert!( @@ -351,7 +351,7 @@ fn preview_and_assignment_scan_tolerate_corrupt_historical_payloads() { ("task_assigned", "also-not-json"), ] { conn.execute( - "INSERT INTO agent_inbox ( + "INSERT INTO agent_org_runtime_inbox ( recipient_agent_id, recipient_member_id, sender_agent_id, org_run_id, payload_kind, payload_json, created_at ) VALUES ('worker', 'member-worker', 'sender', ?1, ?2, ?3, ?4)", @@ -359,7 +359,7 @@ fn preview_and_assignment_scan_tolerate_corrupt_historical_payloads() { ) .expect("seed corrupt historical inbox row"); } - conn.execute_batch("DROP INDEX idx_agent_inbox_run_task_assignment_v4") + conn.execute_batch("DROP INDEX idx_agent_org_runtime_inbox_run_task_assignment_v4") .expect("drop assignment index to simulate upgrade"); init_schema(&conn).expect("schema upgrade tolerates corrupt historical payloads"); AgentInboxStore::insert(InsertInboxParams { @@ -398,12 +398,13 @@ fn preview_and_assignment_scan_tolerate_corrupt_historical_payloads() { fn open_assignment_snapshot_uses_current_tasks_and_expression_index() { let _sandbox = sandbox_with_inbox_schema(); let conn = get_connection().expect("test database"); + crate::coordination::agent_org_runs::init_schema(&conn).expect("run schema"); crate::coordination::agent_org_tasks::init_schema(&conn).expect("task schema"); let run_id = format!("run-{}", uuid::Uuid::new_v4()); let now = chrono::Utc::now().to_rfc3339(); for (task_id, status) in [("open-task", "pending"), ("done-task", "completed")] { conn.execute( - "INSERT INTO agent_org_tasks + "INSERT INTO agent_org_runtime_tasks (id, org_run_id, subject, description, status, owner, blocks_json, blocked_by_json, created_at, updated_at) VALUES (?1, ?2, ?1, '', ?3, 'member-worker', '[]', '[]', ?4, ?4)", @@ -449,7 +450,7 @@ fn open_assignment_snapshot_uses_current_tasks_and_expression_index() { assert!( details .iter() - .any(|detail| detail.contains("idx_agent_inbox_run_task_assignment_v4")), + .any(|detail| detail.contains("idx_agent_org_runtime_inbox_run_task_assignment_v4")), "assignment lookup must use the expression index: {details:?}" ); assert!( @@ -464,12 +465,13 @@ fn open_assignment_snapshot_uses_current_tasks_and_expression_index() { fn assignment_snapshot_requires_current_owner_and_valid_typed_payload() { let _sandbox = sandbox_with_inbox_schema(); let conn = get_connection().expect("test database"); + crate::coordination::agent_org_runs::init_schema(&conn).expect("run schema"); crate::coordination::agent_org_tasks::init_schema(&conn).expect("task schema"); let run_id = format!("run-{}", uuid::Uuid::new_v4()); let now = chrono::Utc::now().to_rfc3339(); for task_id in ["reassigned-task", "{}"] { conn.execute( - "INSERT INTO agent_org_tasks + "INSERT INTO agent_org_runtime_tasks (id, org_run_id, subject, description, status, owner, blocks_json, blocked_by_json, created_at, updated_at) VALUES (?1, ?2, ?1, '', 'pending', 'member-b', '[]', '[]', ?3, ?3)", @@ -500,7 +502,7 @@ fn assignment_snapshot_requires_current_owner_and_valid_typed_payload() { // Valid JSON with the right tag/id but missing required fields is not // a real TaskAssigned envelope and cannot suppress recovery. conn.execute( - "INSERT INTO agent_inbox ( + "INSERT INTO agent_org_runtime_inbox ( recipient_agent_id, recipient_member_id, sender_agent_id, org_run_id, payload_kind, payload_json, created_at ) VALUES ('worker-b', 'member-b', 'coordinator', ?1, @@ -515,7 +517,7 @@ fn assignment_snapshot_requires_current_owner_and_valid_typed_payload() { // A non-text task_id must not collide with the literal task id "{}". conn.execute( - "INSERT INTO agent_inbox ( + "INSERT INTO agent_org_runtime_inbox ( recipient_agent_id, recipient_member_id, sender_agent_id, org_run_id, payload_kind, payload_json, created_at ) VALUES ('worker-b', 'member-b', 'coordinator', ?1, @@ -830,7 +832,7 @@ fn stale_session_cannot_ack_another_sessions_materialization() { .expect("insert inbox row"); let conn = get_connection().expect("db"); conn.execute( - "INSERT INTO agent_inbox_materializations + "INSERT INTO agent_org_runtime_inbox_materializations (inbox_id, session_id, transcript_message_id, transcript_intent_id, materialized_at) VALUES (?1, 'new-session', 'message', 'intent', ?2)", params![row.id, chrono::Utc::now().to_rfc3339()], diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_member_interventions.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_member_interventions.rs index 0a029657e3..5b306e88a9 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_member_interventions.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_member_interventions.rs @@ -7,7 +7,9 @@ use rusqlite::{params, Connection, OptionalExtension, Result as SqliteResult}; use serde::Serialize; -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use super::agent_org_runs::COORDINATOR_MEMBER_ID; @@ -56,9 +58,16 @@ pub struct AgentMemberInterventionRecord { pub cleared_at: Option, } +/// Tests-only convenience: production initialization goes through the +/// namespace coordinator (`coordination::schema::initialize`), never this +/// module-level entry point. pub fn init_schema(conn: &Connection) -> SqliteResult<()> { + create_schema(conn) +} + +pub(crate) fn create_schema(conn: &Connection) -> SqliteResult<()> { conn.execute_batch( - "CREATE TABLE IF NOT EXISTS agent_member_interventions ( + "CREATE TABLE IF NOT EXISTS agent_org_runtime_member_interventions ( org_run_id TEXT NOT NULL, member_id TEXT NOT NULL, agent_id TEXT NOT NULL, @@ -71,10 +80,10 @@ pub fn init_schema(conn: &Connection) -> SqliteResult<()> { cleared_at TEXT, PRIMARY KEY (org_run_id, member_id) ); - CREATE INDEX IF NOT EXISTS idx_agent_member_interventions_session - ON agent_member_interventions(session_id); - CREATE INDEX IF NOT EXISTS idx_agent_member_interventions_active - ON agent_member_interventions(org_run_id, cleared_at, resume_after);", + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_member_interventions_session + ON agent_org_runtime_member_interventions(session_id); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_member_interventions_active + ON agent_org_runtime_member_interventions(org_run_id, cleared_at, resume_after);", ) } @@ -111,9 +120,9 @@ impl AgentMemberInterventionStore { let status = MemberInterventionStatus::UserIntervention; with_sessions_writer(|| -> Result<(), String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; conn.execute( - "INSERT INTO agent_member_interventions ( + "INSERT INTO agent_org_runtime_member_interventions ( org_run_id, member_id, agent_id, @@ -151,7 +160,7 @@ impl AgentMemberInterventionStore { let record = Self::get(¶ms.org_run_id, ¶ms.member_id)?.ok_or_else(|| { format!( - "agent_member_interventions upsert did not return row for run={} member={}", + "agent_org_runtime_member_interventions upsert did not return row for run={} member={}", params.org_run_id, params.member_id ) })?; @@ -162,10 +171,10 @@ impl AgentMemberInterventionStore { pub fn clear(org_run_id: &str, member_id: &str) -> Result { let now = chrono::Utc::now().to_rfc3339(); let changed = with_sessions_writer(|| -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let updated = conn .execute( - "UPDATE agent_member_interventions + "UPDATE agent_org_runtime_member_interventions SET cleared_at = ?3 WHERE org_run_id = ?1 AND member_id = ?2 AND cleared_at IS NULL", params![org_run_id, member_id, now], @@ -191,13 +200,13 @@ impl AgentMemberInterventionStore { ) -> Result<(bool, Option), String> { let now = chrono::Utc::now().to_rfc3339(); with_sessions_writer(|| -> Result<(bool, Option), String> { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; let updated = tx .execute( - "UPDATE agent_member_interventions + "UPDATE agent_org_runtime_member_interventions SET cleared_at = ?3 WHERE org_run_id = ?1 AND member_id = ?2 AND cleared_at IS NULL", params![org_run_id, member_id, now], @@ -205,13 +214,13 @@ impl AgentMemberInterventionStore { .map_err(|err| err.to_string())?; let boundary = tx .query_row( - "SELECT MAX(id) FROM agent_inbox + "SELECT MAX(id) FROM agent_org_runtime_inbox WHERE recipient_member_id=?1 AND org_run_id=?2 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id )", params![member_id, org_run_id], |row| row.get(0), @@ -226,7 +235,7 @@ impl AgentMemberInterventionStore { org_run_id: &str, member_id: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; conn.query_row( "SELECT org_run_id, member_id, @@ -238,7 +247,7 @@ impl AgentMemberInterventionStore { last_user_activity_at, resume_after, cleared_at - FROM agent_member_interventions + FROM agent_org_runtime_member_interventions WHERE org_run_id = ?1 AND member_id = ?2", params![org_run_id, member_id], row_to_intervention, @@ -274,9 +283,9 @@ impl AgentMemberInterventionStore { pub fn clear_expired_and_legacy() -> Result { let now = chrono::Utc::now().to_rfc3339(); with_sessions_writer(|| -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; conn.execute( - "UPDATE agent_member_interventions + "UPDATE agent_org_runtime_member_interventions SET cleared_at = ?1 WHERE cleared_at IS NULL AND ( @@ -300,10 +309,10 @@ impl AgentMemberInterventionStore { pub fn clear_all_active_on_startup() -> Result { let now = chrono::Utc::now().to_rfc3339(); with_sessions_writer(|| -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let updated = conn .execute( - "UPDATE agent_member_interventions + "UPDATE agent_org_runtime_member_interventions SET cleared_at = ?1 WHERE cleared_at IS NULL", params![now], @@ -314,7 +323,7 @@ impl AgentMemberInterventionStore { } pub fn list_active(org_run_id: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::list_active_with_connection(&conn, org_run_id) } @@ -335,7 +344,7 @@ impl AgentMemberInterventionStore { last_user_activity_at, resume_after, cleared_at - FROM agent_member_interventions + FROM agent_org_runtime_member_interventions WHERE org_run_id = ?1 AND cleared_at IS NULL AND member_id <> ?3 @@ -393,9 +402,9 @@ mod tests { fn setup() -> test_helpers::test_env::SandboxGuard { let sandbox = test_helpers::test_env::sandbox(); - let conn = get_connection().expect("db connection"); + let conn = runtime_connection().expect("db connection"); init_schema(&conn).expect("schema"); - conn.execute("DELETE FROM agent_member_interventions", []) + conn.execute("DELETE FROM agent_org_runtime_member_interventions", []) .expect("clear"); sandbox } @@ -453,9 +462,9 @@ mod tests { fn legacy_coordinator_intervention_is_hidden_without_mutating_on_read() { let _sandbox = setup(); let now = chrono::Utc::now(); - let conn = get_connection().expect("db connection"); + let conn = runtime_connection().expect("db connection"); conn.execute( - "INSERT INTO agent_member_interventions ( + "INSERT INTO agent_org_runtime_member_interventions ( org_run_id, member_id, agent_id, session_id, status, reason, entered_at, last_user_activity_at, resume_after, cleared_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?7, ?8, NULL)", @@ -553,7 +562,7 @@ mod tests { #[test] fn active_for_member_returns_none_after_ttl_expires() { let _sandbox = setup(); - let conn = get_connection().expect("db connection"); + let conn = runtime_connection().expect("db connection"); AgentMemberInterventionStore::enter(EnterMemberInterventionParams { org_run_id: "run-1".into(), @@ -568,7 +577,7 @@ mod tests { // Backdated resume_after to simulate expiry without sleeping. let expired = (chrono::Utc::now() - chrono::Duration::seconds(10)).to_rfc3339(); conn.execute( - "UPDATE agent_member_interventions SET resume_after = ?1 WHERE org_run_id = ?2 AND member_id = ?3", + "UPDATE agent_org_runtime_member_interventions SET resume_after = ?1 WHERE org_run_id = ?2 AND member_id = ?3", rusqlite::params![expired, "run-1", "member-ttl"], ) .expect("backdate"); diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/artifact.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/artifact.rs index b733723e7d..77ecb9794e 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/artifact.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/artifact.rs @@ -4,7 +4,9 @@ use std::sync::OnceLock; use rusqlite::{params, Connection, OptionalExtension}; -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use super::persistence::query_record; use super::AgentOrgPlanApproval; @@ -427,11 +429,11 @@ pub(super) fn list_distinct_plan_paths_after( after_path: Option<&str>, limit: usize, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( "SELECT DISTINCT plan_path - FROM agent_org_plan_approvals + FROM agent_org_runtime_plan_approvals WHERE (?1 IS NULL OR plan_path > ?1) ORDER BY plan_path ASC LIMIT ?2", @@ -458,7 +460,7 @@ fn latest_plan_revision_for_path_with_connection( } fn latest_plan_revision_for_path(plan_path: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; latest_plan_revision_for_path_with_connection(&conn, plan_path) } @@ -467,7 +469,7 @@ fn stage_plan_artifact_if_needed( plan_path: &str, canonical_content: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let Some(owned) = owned_plan_path_for_existing_revision_with_connection(&conn, source_session_id, plan_path)? else { @@ -526,7 +528,7 @@ pub(super) fn repair_latest_plan_artifact_for_path(plan_path: &str) -> Result Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let latest = latest_plan_revision_for_path_with_connection(&conn, plan_path)?; let still_current = latest.as_ref().is_some_and(|record| { record.approval_id == canonical.approval_id diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/mod.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/mod.rs index 69406d9d13..beedf91a2d 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/mod.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/mod.rs @@ -148,9 +148,16 @@ pub struct AgentOrgPlanInboxDelivery { pub sender_member_id: Option, } +/// Tests-only convenience: production initialization goes through the +/// namespace coordinator (`coordination::schema::initialize`), never this +/// module-level entry point. pub fn init_schema(conn: &Connection) -> rusqlite::Result<()> { + create_schema(conn) +} + +pub(crate) fn create_schema(conn: &Connection) -> rusqlite::Result<()> { conn.execute_batch( - "CREATE TABLE IF NOT EXISTS agent_org_plan_approvals ( + "CREATE TABLE IF NOT EXISTS agent_org_runtime_plan_approvals ( approval_id TEXT PRIMARY KEY, plan_revision_id TEXT NOT NULL UNIQUE, request_id TEXT NOT NULL UNIQUE, @@ -169,10 +176,10 @@ pub fn init_schema(conn: &Connection) -> rusqlite::Result<()> { created_at TEXT NOT NULL, resolved_at TEXT ); - CREATE INDEX IF NOT EXISTS idx_agent_org_plan_approvals_run_status - ON agent_org_plan_approvals(org_run_id, status, created_at); - CREATE INDEX IF NOT EXISTS idx_agent_org_plan_approvals_task - ON agent_org_plan_approvals(org_run_id, source_task_id, created_at);", + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_plan_approvals_run_status + ON agent_org_runtime_plan_approvals(org_run_id, status, created_at); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_plan_approvals_task + ON agent_org_runtime_plan_approvals(org_run_id, source_task_id, created_at);", )?; Ok(()) } diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/persistence.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/persistence.rs index 146068a9a9..5a66a6f316 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/persistence.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/persistence.rs @@ -9,7 +9,7 @@ pub(super) fn insert_record( approval: &AgentOrgPlanApproval, ) -> Result<(), String> { conn.execute( - "INSERT INTO agent_org_plan_approvals ( + "INSERT INTO agent_org_runtime_plan_approvals ( approval_id, plan_revision_id, request_id, org_run_id, source_task_id, source_member_id, source_session_id, root_session_id, policy, status, plan_title, plan_path, plan_content, decision_by, @@ -49,7 +49,7 @@ pub(super) fn query_record( source_task_id, source_member_id, source_session_id, root_session_id, policy, status, plan_title, plan_path, plan_content, decision_by, feedback, created_at, resolved_at - FROM agent_org_plan_approvals {where_clause} LIMIT 1" + FROM agent_org_runtime_plan_approvals {where_clause} LIMIT 1" ); conn.query_row(&sql, params, row_to_record) .optional() diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/store.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/store.rs index c78a03b3f6..375c976483 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/store.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/store.rs @@ -2,7 +2,9 @@ use std::path::PathBuf; use rusqlite::{params, Connection, TransactionBehavior}; -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use crate::coordination::agent_inbox::{ AgentInboxRecord, AgentInboxStore, AgentMessage, InsertInboxParams, RequestId, @@ -50,7 +52,7 @@ impl AgentOrgPlanApprovalStore { file_name: &str, ) -> Result { validate_plan_file_name(file_name)?; - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let (root, _) = expected_plan_root_with_connection(&conn, source_session_id)?; Ok(root.join(file_name)) } @@ -63,7 +65,7 @@ impl AgentOrgPlanApprovalStore { source_session_id: &str, plan_path: &str, ) -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let owned = match validate_owned_plan_path_with_connection(&conn, source_session_id, plan_path) { Ok(owned) => owned, @@ -108,7 +110,7 @@ impl AgentOrgPlanApprovalStore { params: CreateAgentOrgPlanApprovalParams, ) -> Result { validate_create_params(¶ms)?; - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let staged_artifact = Some(stage_plan_artifact_with_connection( &conn, ¶ms.source_session_id, @@ -144,7 +146,7 @@ impl AgentOrgPlanApprovalStore { } validate_delivery(&delivery)?; validate_create_params(¶ms)?; - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let staged_artifact = Some(stage_plan_artifact_with_connection( &conn, ¶ms.source_session_id, @@ -189,7 +191,7 @@ impl AgentOrgPlanApprovalStore { return Err("automatic plan approval requires automatic policy".to_string()); } validate_create_params(¶ms)?; - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let staged_artifact = Some(stage_plan_artifact_with_connection( &conn, ¶ms.source_session_id, @@ -220,14 +222,14 @@ impl AgentOrgPlanApprovalStore { } pub fn list_pending_by_run(run_id: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( "SELECT approval_id, plan_revision_id, request_id, org_run_id, source_task_id, source_member_id, source_session_id, root_session_id, policy, status, plan_title, plan_path, plan_content, decision_by, feedback, created_at, resolved_at - FROM agent_org_plan_approvals + FROM agent_org_runtime_plan_approvals WHERE org_run_id=?1 AND status=?2 ORDER BY created_at ASC, approval_id ASC", ) @@ -244,10 +246,10 @@ impl AgentOrgPlanApprovalStore { /// Lightweight watchdog projection. Plan Markdown can be hundreds of KB; /// recovery only needs to know which task ids are waiting for approval. pub fn pending_source_task_ids_by_run(run_id: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( - "SELECT source_task_id FROM agent_org_plan_approvals + "SELECT source_task_id FROM agent_org_runtime_plan_approvals WHERE org_run_id=?1 AND status=?2 ORDER BY created_at ASC, approval_id ASC", ) @@ -265,7 +267,7 @@ impl AgentOrgPlanApprovalStore { pub fn list_pending_summaries_by_run( run_id: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::list_pending_summaries_by_run_with_connection(&conn, run_id) } @@ -280,7 +282,7 @@ impl AgentOrgPlanApprovalStore { source_task_id, source_member_id, source_session_id, root_session_id, policy, status, plan_title, length(CAST(plan_content AS BLOB)), created_at - FROM agent_org_plan_approvals + FROM agent_org_runtime_plan_approvals WHERE org_run_id=?1 AND status=?2 ORDER BY created_at ASC, approval_id ASC", ) @@ -298,7 +300,7 @@ impl AgentOrgPlanApprovalStore { run_id: &str, request_id: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; query_record( &conn, "WHERE org_run_id=?1 AND request_id=?2 AND status='pending'", @@ -316,7 +318,7 @@ impl AgentOrgPlanApprovalStore { run_id: &str, request_id: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; query_record( &conn, "WHERE org_run_id=?1 AND request_id=?2", @@ -338,7 +340,7 @@ impl AgentOrgPlanApprovalStore { PLAN_CONTENT_MAX_BYTES, )?; } - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let current = query_record(&conn, "WHERE approval_id=?1", params![approval_id])? .ok_or_else(|| format!("agent_org_plan_approval_not_found: {approval_id}"))?; authorize_decision(current.policy, decision_by)?; @@ -436,7 +438,7 @@ impl AgentOrgPlanApprovalStore { )?; validate_delivery(&delivery)?; let result = with_sessions_writer(|| { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -449,7 +451,7 @@ impl AgentOrgPlanApprovalStore { authorize_decision(approval.policy, decision_by)?; let run_status: String = tx .query_row( - "SELECT status FROM agent_org_runs WHERE id=?1", + "SELECT status FROM agent_org_runtime_runs WHERE id=?1", params![&approval.org_run_id], |row| row.get(0), ) @@ -463,7 +465,7 @@ impl AgentOrgPlanApprovalStore { let resolved_at = chrono::Utc::now().to_rfc3339(); let changed = tx .execute( - "UPDATE agent_org_plan_approvals + "UPDATE agent_org_runtime_plan_approvals SET status=?1, decision_by=?2, feedback=?3, resolved_at=?4 WHERE approval_id=?5 AND plan_revision_id=?6 AND status=?7", params![ @@ -515,7 +517,7 @@ impl AgentOrgPlanApprovalStore { } pub fn get(approval_id: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; query_record(&conn, "WHERE approval_id=?1", params![approval_id]) } @@ -530,7 +532,7 @@ impl AgentOrgPlanApprovalStore { approval_id: &str, plan_revision_id: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let record = query_record( &conn, "WHERE approval_id=?1 AND plan_revision_id=?2", @@ -560,7 +562,7 @@ impl AgentOrgPlanApprovalStore { approval_id: &str, plan_revision_id: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let record = query_record( &conn, "WHERE org_run_id=?1 AND approval_id=?2 AND plan_revision_id=?3", @@ -625,20 +627,20 @@ impl AgentOrgPlanApprovalStore { pub fn cancel_pending_for_terminal_or_missing_runs() -> Result { let (changed, run_ids) = with_sessions_writer(|| -> Result<(usize, Vec), String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let run_ids = { let mut stmt = conn .prepare( "SELECT DISTINCT approval.org_run_id - FROM agent_org_plan_approvals approval + FROM agent_org_runtime_plan_approvals approval WHERE approval.status=?1 AND ( NOT EXISTS ( - SELECT 1 FROM agent_org_runs run + SELECT 1 FROM agent_org_runtime_runs run WHERE run.id=approval.org_run_id ) OR EXISTS ( - SELECT 1 FROM agent_org_runs run + SELECT 1 FROM agent_org_runtime_runs run WHERE run.id=approval.org_run_id AND run.status IN ('failed','archived') ) @@ -656,17 +658,17 @@ impl AgentOrgPlanApprovalStore { }; let changed = conn .execute( - "UPDATE agent_org_plan_approvals + "UPDATE agent_org_runtime_plan_approvals SET status=?1, decision_by='system', resolved_at=?2 WHERE status=?3 AND ( NOT EXISTS ( - SELECT 1 FROM agent_org_runs run - WHERE run.id=agent_org_plan_approvals.org_run_id + SELECT 1 FROM agent_org_runtime_runs run + WHERE run.id=agent_org_runtime_plan_approvals.org_run_id ) OR EXISTS ( - SELECT 1 FROM agent_org_runs run - WHERE run.id=agent_org_plan_approvals.org_run_id + SELECT 1 FROM agent_org_runtime_runs run + WHERE run.id=agent_org_runtime_plan_approvals.org_run_id AND run.status IN ('failed','archived') ) )", diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/tests.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/tests.rs index b10061fe0c..5aca36d721 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/tests.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/tests.rs @@ -230,18 +230,18 @@ fn approval_dispatches_task_from_legacy_blocks_only_edge() { let conn = get_connection().expect("test sqlite connection"); conn.execute( - "UPDATE agent_org_tasks SET blocks_json='[\"legacy-build-task\"]' + "UPDATE agent_org_runtime_tasks SET blocks_json='[\"legacy-build-task\"]' WHERE org_run_id=?1 AND id='plan-task'", params![&context.run_id], ) .expect("seed legacy upstream blocks edge"); conn.execute( - "UPDATE agent_org_tasks SET blocked_by_json='[]' + "UPDATE agent_org_runtime_tasks SET blocked_by_json='[]' WHERE org_run_id=?1 AND id='legacy-build-task'", params![&context.run_id], ) .expect("preserve legacy blocks-only representation"); - conn.execute("DELETE FROM agent_inbox", []) + conn.execute("DELETE FROM agent_org_runtime_inbox", []) .expect("remove create-time assignment noise"); let pending = create_pending_approval(&context); @@ -398,7 +398,7 @@ fn watchdog_pending_task_projection_never_materializes_plan_markdown() { get_connection() .unwrap() .execute( - "UPDATE agent_org_plan_approvals + "UPDATE agent_org_runtime_plan_approvals SET plan_content=CAST(X'80' AS TEXT) WHERE approval_id=?1", params![&pending.approval_id], @@ -417,7 +417,7 @@ fn coordinator_request_insert_failure_rolls_back_pending_creation() { create_plan_task(&context); get_connection() .expect("test db") - .execute("DROP TABLE agent_inbox", []) + .execute("DROP TABLE agent_org_runtime_inbox", []) .expect("remove inbox to force request delivery failure"); let params = approval_params(&context); @@ -643,7 +643,7 @@ fn feedback_insert_failure_rolls_back_changes_requested_status() { let pending = create_pending_approval(&context); get_connection() .expect("test db") - .execute("DROP TABLE agent_inbox", []) + .execute("DROP TABLE agent_org_runtime_inbox", []) .expect("remove inbox to force delivery failure"); AgentOrgPlanApprovalStore::request_changes( diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/transitions.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/transitions.rs index 6e7337a93a..9bc27a6ba4 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/transitions.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_plan_approvals/transitions.rs @@ -30,7 +30,7 @@ pub(super) fn create_pending_in_tx( validate_owned_plan_path_with_connection(tx, ¶ms.source_session_id, ¶ms.plan_path)?; let run_status: Option = tx .query_row( - "SELECT status FROM agent_org_runs WHERE id=?1", + "SELECT status FROM agent_org_runtime_runs WHERE id=?1", params![¶ms.org_run_id], |row| row.get(0), ) @@ -46,7 +46,7 @@ pub(super) fn create_pending_in_tx( let task: Option<(Option, String, Option)> = tx .query_row( - "SELECT owner, status, metadata_json FROM agent_org_tasks + "SELECT owner, status, metadata_json FROM agent_org_runtime_tasks WHERE org_run_id=?1 AND id=?2", params![¶ms.org_run_id, ¶ms.source_task_id], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), @@ -88,7 +88,7 @@ pub(super) fn create_pending_in_tx( let now = chrono::Utc::now().to_rfc3339(); tx.execute( - "UPDATE agent_org_plan_approvals + "UPDATE agent_org_runtime_plan_approvals SET status=?1, resolved_at=?2 WHERE org_run_id=?3 AND source_task_id=?4 AND status=?5", params![ @@ -166,7 +166,7 @@ pub(super) fn approve_pending_in_tx( let resolved_at = chrono::Utc::now().to_rfc3339(); let changed = tx .execute( - "UPDATE agent_org_plan_approvals + "UPDATE agent_org_runtime_plan_approvals SET status=?1, decision_by=?2, plan_content=?3, resolved_at=?4 WHERE approval_id=?5 AND plan_revision_id=?6 AND status=?7", params![ @@ -292,7 +292,7 @@ fn participant_agent_ids_in_tx( let (coordinator_agent_id, snapshot_json): (String, Option) = tx .query_row( "SELECT coordinator_agent_id, org_snapshot_json - FROM agent_org_runs WHERE id=?1 AND status='running'", + FROM agent_org_runtime_runs WHERE id=?1 AND status='running'", params![run_id], |row| Ok((row.get(0)?, row.get(1)?)), ) diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/helpers.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/helpers.rs index 8b28063fe4..9ee1947bf0 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/helpers.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/helpers.rs @@ -3,7 +3,7 @@ use rusqlite::{params, Connection, OptionalExtension, Result as SqliteResult}; use crate::definitions::orgs::{ validate_launch_snapshot, AgentOrgCapabilityIndex, AgentOrgLaunchSnapshot, }; -use database::db::get_connection; +use crate::coordination::availability::runtime_connection; use super::{ AgentOrgContextMember, AgentOrgRunContext, AgentOrgRunEntryMode, AgentOrgRunRecord, @@ -19,7 +19,7 @@ use super::{ /// but has no parent". Both cases terminate the walk identically; /// distinguishing them would not change the resolver outcome. pub(super) fn parent_session_id_of(session_id: &str) -> SqliteResult> { - let conn = get_connection()?; + let conn = runtime_connection()?; let parent = conn .query_row( "SELECT parent_session_id FROM agent_sessions WHERE session_id = ?1", @@ -42,7 +42,7 @@ pub(super) fn parent_session_id_of(session_id: &str) -> SqliteResult SqliteResult> { - let conn = get_connection()?; + let conn = runtime_connection()?; conn.query_row( "SELECT id, org_id, @@ -63,7 +63,7 @@ pub(super) fn load_by_id(run_id: &str) -> SqliteResult created_at, updated_at, idled_at - FROM agent_org_runs + FROM agent_org_runtime_runs WHERE id = ?1 LIMIT 1", params![run_id], @@ -75,7 +75,7 @@ pub(super) fn load_by_id(run_id: &str) -> SqliteResult pub(super) fn load_by_root_session( root_session_id: &str, ) -> SqliteResult> { - let conn = get_connection()?; + let conn = runtime_connection()?; conn.query_row( "SELECT id, org_id, @@ -96,7 +96,7 @@ pub(super) fn load_by_root_session( created_at, updated_at, idled_at - FROM agent_org_runs + FROM agent_org_runtime_runs WHERE root_session_id = ?1 ORDER BY created_at DESC LIMIT 1", @@ -204,7 +204,7 @@ pub(super) fn flatten_members( pub(super) fn insert_run(conn: &Connection, run: &AgentOrgRunRecord) -> SqliteResult<()> { conn.execute( - "INSERT INTO agent_org_runs ( + "INSERT INTO agent_org_runtime_runs ( id, org_id, coordinator_agent_id, diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/materialization.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/materialization.rs index 18c517334a..41b6033c80 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/materialization.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/materialization.rs @@ -138,7 +138,7 @@ pub struct CreateAgentOrgInitialInput { pub(super) fn init_schema(conn: &Connection) -> rusqlite::Result<()> { conn.execute_batch( - "CREATE TABLE IF NOT EXISTS agent_org_member_materializations ( + "CREATE TABLE IF NOT EXISTS agent_org_runtime_member_materializations ( org_run_id TEXT NOT NULL, member_id TEXT NOT NULL, agent_id TEXT NOT NULL, @@ -156,12 +156,12 @@ pub(super) fn init_schema(conn: &Connection) -> rusqlite::Result<()> { updated_at TEXT NOT NULL, PRIMARY KEY(org_run_id, member_id, generation), UNIQUE(org_run_id, session_id), - FOREIGN KEY(org_run_id) REFERENCES agent_org_runs(id) ON DELETE CASCADE + FOREIGN KEY(org_run_id) REFERENCES agent_org_runtime_runs(id) ON DELETE CASCADE ); - CREATE INDEX IF NOT EXISTS idx_agent_org_materializations_pending - ON agent_org_member_materializations(status, org_run_id, generation); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_member_materializations_pending + ON agent_org_runtime_member_materializations(status, org_run_id, generation); - CREATE TABLE IF NOT EXISTS agent_org_initial_inputs ( + CREATE TABLE IF NOT EXISTS agent_org_runtime_initial_inputs ( org_run_id TEXT PRIMARY KEY, turn_intent_id TEXT NOT NULL, message_id TEXT NOT NULL, @@ -174,10 +174,10 @@ pub(super) fn init_schema(conn: &Connection) -> rusqlite::Result<()> { updated_at TEXT NOT NULL, UNIQUE(turn_intent_id), UNIQUE(message_id), - FOREIGN KEY(org_run_id) REFERENCES agent_org_runs(id) ON DELETE CASCADE + FOREIGN KEY(org_run_id) REFERENCES agent_org_runtime_runs(id) ON DELETE CASCADE ); - CREATE INDEX IF NOT EXISTS idx_agent_org_initial_inputs_dispatch - ON agent_org_initial_inputs(status, org_run_id);", + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_initial_inputs_dispatch + ON agent_org_runtime_initial_inputs(status, org_run_id);", ) } @@ -194,7 +194,7 @@ pub(super) fn insert_materialization_intent( AgentOrgMaterializationStatus::Pending }; conn.execute( - "INSERT INTO agent_org_member_materializations ( + "INSERT INTO agent_org_runtime_member_materializations ( org_run_id, member_id, agent_id, generation, session_id, authority_class, status, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, 'starting', ?6, ?7, ?7)", @@ -219,7 +219,7 @@ pub(super) fn insert_initial_input( now: &str, ) -> Result<(), String> { conn.execute( - "INSERT INTO agent_org_initial_inputs ( + "INSERT INTO agent_org_runtime_initial_inputs ( org_run_id, turn_intent_id, message_id, content, payload_json, status, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?7)", @@ -246,7 +246,7 @@ pub(super) fn list_materializations_with_connection( "SELECT org_run_id, member_id, agent_id, generation, session_id, authority_class, status, error_code, error_json, created_at, updated_at - FROM agent_org_member_materializations + FROM agent_org_runtime_member_materializations WHERE org_run_id=?1 ORDER BY member_id ASC", ) @@ -265,7 +265,7 @@ pub(super) fn load_initial_input_with_connection( conn.query_row( "SELECT org_run_id, turn_intent_id, message_id, content, payload_json, status, created_at, updated_at - FROM agent_org_initial_inputs WHERE org_run_id=?1", + FROM agent_org_runtime_initial_inputs WHERE org_run_id=?1", [org_run_id], row_to_initial_input, ) @@ -280,7 +280,7 @@ pub(super) fn load_initial_input_by_turn_with_connection( conn.query_row( "SELECT org_run_id, turn_intent_id, message_id, content, payload_json, status, created_at, updated_at - FROM agent_org_initial_inputs WHERE turn_intent_id=?1", + FROM agent_org_runtime_initial_inputs WHERE turn_intent_id=?1", [turn_intent_id], row_to_initial_input, ) @@ -303,8 +303,8 @@ pub(super) fn list_recoverable_initial_inputs_with_connection( initial.message_id, initial.content, initial.payload_json, initial.status, initial.created_at, initial.updated_at - FROM agent_org_initial_inputs initial - JOIN agent_org_runs run ON run.id=initial.org_run_id + FROM agent_org_runtime_initial_inputs initial + JOIN agent_org_runtime_runs run ON run.id=initial.org_run_id JOIN session_turn_intents turn ON turn.org_run_id=initial.org_run_id AND turn.turn_intent_id=initial.turn_intent_id diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/mod.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/mod.rs index a6437d2a85..273c3a97c3 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/mod.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/mod.rs @@ -45,26 +45,6 @@ use crate::definitions::orgs::{ AgentOrgCapabilityIndex, AgentOrgLaunchSnapshot, PlanApprovalPolicy, }; -type RunSchemaColumn = (i64, String, String, i64, Option, i64); - -const LEGACY_RUN_SCHEMA: [(&str, &str, i64, Option<&str>, i64); 15] = [ - ("id", "TEXT", 0, None, 1), - ("org_id", "TEXT", 1, None, 0), - ("coordinator_agent_id", "TEXT", 1, None, 0), - ("root_session_id", "TEXT", 0, None, 0), - ("org_snapshot_json", "TEXT", 0, None, 0), - ("entry_mode", "TEXT", 1, None, 0), - ("status", "TEXT", 1, None, 0), - ("work_item_id", "TEXT", 0, None, 0), - ("project_slug", "TEXT", 0, None, 0), - ("routine_fire_id", "TEXT", 0, None, 0), - ("summary", "TEXT", 0, None, 0), - ("last_error", "TEXT", 0, None, 0), - ("created_at", "TEXT", 1, None, 0), - ("updated_at", "TEXT", 1, None, 0), - ("completed_at", "TEXT", 0, None, 0), -]; - pub use core_types::agent_org::COORDINATOR_MEMBER_ID; pub(crate) const DEFAULT_COORDINATOR_DISPLAY_NAME: &str = "Coordinator"; @@ -407,65 +387,19 @@ impl AgentOrgStartingFailure { } } -/// Initialize runtime Agent Org tables in `sessions.db`. +/// Initialize the redesigned runtime run envelope in an already-isolated +/// namespace. +/// +/// Tests-only convenience: production initialization goes through the +/// namespace coordinator (`coordination::schema::initialize`), never this +/// module-level entry point. pub fn init_schema(conn: &Connection) -> SqliteResult<()> { - let columns = conn - .prepare( - "SELECT cid, name, type, \"notnull\", dflt_value, pk - FROM pragma_table_info('agent_org_runs') ORDER BY cid", - )? - .query_map([], |row| { - Ok(( - row.get(0)?, - row.get(1)?, - row.get(2)?, - row.get(3)?, - row.get(4)?, - row.get(5)?, - )) - })? - .collect::>>()?; - - let is_legacy_run_schema = columns.len() == LEGACY_RUN_SCHEMA.len() - && columns - .iter() - .zip(LEGACY_RUN_SCHEMA.iter()) - .enumerate() - .all(|(cid, (actual, expected))| { - actual.0 == cid as i64 - && actual.1 == expected.0 - && actual.2 == expected.1 - && actual.3 == expected.2 - && actual.4.as_deref() == expected.3 - && actual.5 == expected.4 - }); - - if is_legacy_run_schema { - let legacy_run_count: i64 = - conn.query_row("SELECT COUNT(*) FROM agent_org_runs", [], |row| row.get(0))?; - let tx = database::db::begin_immediate(conn)?; - tx.execute_batch( - "DROP TABLE IF EXISTS agent_org_initial_inputs; - DROP TABLE IF EXISTS agent_org_member_materializations; - DROP TABLE IF EXISTS agent_org_run_progress; - DROP TABLE agent_org_runs;", - )?; - create_canonical_schema(&tx)?; - tx.commit()?; - tracing::info!( - event = "agent_org_legacy_run_schema_reset", - legacy_run_count, - "reset legacy Agent Org runtime envelope" - ); - return Ok(()); - } - - create_canonical_schema(conn) + create_schema(conn) } -fn create_canonical_schema(conn: &Connection) -> SqliteResult<()> { +pub(crate) fn create_schema(conn: &Connection) -> SqliteResult<()> { conn.execute_batch( - "CREATE TABLE IF NOT EXISTS agent_org_runs ( + "CREATE TABLE IF NOT EXISTS agent_org_runtime_runs ( id TEXT PRIMARY KEY, org_id TEXT NOT NULL, coordinator_agent_id TEXT NOT NULL, @@ -492,14 +426,14 @@ fn create_canonical_schema(conn: &Connection) -> SqliteResult<()> { updated_at TEXT NOT NULL, idled_at TEXT ); - CREATE INDEX IF NOT EXISTS idx_agent_org_runs_org_updated - ON agent_org_runs(org_id, updated_at); - CREATE INDEX IF NOT EXISTS idx_agent_org_runs_root_session - ON agent_org_runs(root_session_id); - CREATE INDEX IF NOT EXISTS idx_agent_org_runs_work_item - ON agent_org_runs(work_item_id); - CREATE INDEX IF NOT EXISTS idx_agent_org_runs_status - ON agent_org_runs(status);", + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_runs_org_updated + ON agent_org_runtime_runs(org_id, updated_at); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_runs_root_session + ON agent_org_runtime_runs(root_session_id); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_runs_work_item + ON agent_org_runtime_runs(work_item_id); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_runs_status + ON agent_org_runtime_runs(status);", )?; materialization::init_schema(conn)?; progress::init_schema(conn)?; diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/progress.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/progress.rs index 933b860817..9f9b68a6d5 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/progress.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/progress.rs @@ -28,7 +28,7 @@ pub struct AgentOrgRunProgress { pub(super) fn init_schema(conn: &Connection) -> rusqlite::Result<()> { conn.execute_batch( - "CREATE TABLE IF NOT EXISTS agent_org_run_progress ( + "CREATE TABLE IF NOT EXISTS agent_org_runtime_run_progress ( org_run_id TEXT PRIMARY KEY, work_revision INTEGER NOT NULL DEFAULT 0 CHECK(work_revision >= 0), coordinator_presented_work_revision INTEGER, @@ -38,17 +38,17 @@ pub(super) fn init_schema(conn: &Connection) -> rusqlite::Result<()> { completion_requested_work_revision INTEGER, completion_summary TEXT, updated_at TEXT NOT NULL, - FOREIGN KEY(org_run_id) REFERENCES agent_org_runs(id) ON DELETE CASCADE + FOREIGN KEY(org_run_id) REFERENCES agent_org_runtime_runs(id) ON DELETE CASCADE );", )?; // Existing runs predate the revision table. Revision zero means "no // post-migration task mutation observed yet"; subsequent mutations use // the same monotonic bump path as new runs. conn.execute( - "INSERT INTO agent_org_run_progress (org_run_id, updated_at) - SELECT run.id, run.updated_at FROM agent_org_runs run + "INSERT INTO agent_org_runtime_run_progress (org_run_id, updated_at) + SELECT run.id, run.updated_at FROM agent_org_runtime_runs run WHERE NOT EXISTS ( - SELECT 1 FROM agent_org_run_progress progress + SELECT 1 FROM agent_org_runtime_run_progress progress WHERE progress.org_run_id=run.id )", [], @@ -58,7 +58,7 @@ pub(super) fn init_schema(conn: &Connection) -> rusqlite::Result<()> { pub(super) fn ensure_progress_in_conn(conn: &Connection, org_run_id: &str) -> Result<(), String> { conn.execute( - "INSERT INTO agent_org_run_progress (org_run_id, updated_at) + "INSERT INTO agent_org_runtime_run_progress (org_run_id, updated_at) VALUES (?1, ?2) ON CONFLICT(org_run_id) DO NOTHING", params![org_run_id, chrono::Utc::now().to_rfc3339()], @@ -77,7 +77,7 @@ pub(crate) fn bump_work_revision_in_tx( ) -> Result { ensure_progress_in_conn(tx, org_run_id)?; tx.execute( - "UPDATE agent_org_run_progress + "UPDATE agent_org_runtime_run_progress SET work_revision = work_revision + 1, completion_requested = 0, completion_requested_at = NULL, @@ -89,7 +89,7 @@ pub(crate) fn bump_work_revision_in_tx( ) .map_err(|err| err.to_string())?; tx.query_row( - "SELECT work_revision FROM agent_org_run_progress WHERE org_run_id=?1", + "SELECT work_revision FROM agent_org_runtime_run_progress WHERE org_run_id=?1", params![org_run_id], |row| row.get(0), ) @@ -107,7 +107,7 @@ pub(super) fn load_progress_with_conn( completion_requested, completion_requested_at, completion_requested_work_revision, completion_summary, updated_at - FROM agent_org_run_progress + FROM agent_org_runtime_run_progress WHERE org_run_id=?1", params![org_run_id], row_to_progress, @@ -123,7 +123,7 @@ pub(super) fn stage_coordinator_presented_with_conn( ensure_progress_in_conn(conn, org_run_id)?; let run_is_running: bool = conn .query_row( - "SELECT EXISTS(SELECT 1 FROM agent_org_runs WHERE id=?1 AND status='running')", + "SELECT EXISTS(SELECT 1 FROM agent_org_runtime_runs WHERE id=?1 AND status='running')", params![org_run_id], |row| row.get(0), ) @@ -133,13 +133,13 @@ pub(super) fn stage_coordinator_presented_with_conn( } let revision: i64 = conn .query_row( - "SELECT work_revision FROM agent_org_run_progress WHERE org_run_id=?1", + "SELECT work_revision FROM agent_org_runtime_run_progress WHERE org_run_id=?1", params![org_run_id], |row| row.get(0), ) .map_err(|err| err.to_string())?; conn.execute( - "UPDATE agent_org_run_progress + "UPDATE agent_org_runtime_run_progress SET coordinator_presented_work_revision=?2, updated_at=?3 WHERE org_run_id=?1", params![org_run_id, revision, chrono::Utc::now().to_rfc3339()], @@ -161,7 +161,7 @@ pub(super) fn mark_coordinator_observed_revision_with_conn( } let updated = conn .execute( - "UPDATE agent_org_run_progress + "UPDATE agent_org_runtime_run_progress SET coordinator_observed_work_revision = CASE WHEN coordinator_observed_work_revision IS NULL OR ?2 > coordinator_observed_work_revision @@ -185,7 +185,7 @@ pub(super) fn mark_coordinator_observed_revision_with_conn( } conn.query_row( "SELECT coordinator_observed_work_revision - FROM agent_org_run_progress WHERE org_run_id=?1", + FROM agent_org_runtime_run_progress WHERE org_run_id=?1", params![org_run_id], |row| row.get(0), ) @@ -208,7 +208,7 @@ pub(super) fn record_completion_request_in_tx( let now = chrono::Utc::now().to_rfc3339(); let updated = tx .execute( - "UPDATE agent_org_run_progress + "UPDATE agent_org_runtime_run_progress SET completion_requested=1, completion_requested_at=?2, completion_requested_work_revision=work_revision, diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/quiescence.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/quiescence.rs index 2e70a7d234..1ddc0b0631 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/quiescence.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/quiescence.rs @@ -296,12 +296,12 @@ pub(crate) fn guaranteed_current_turn_effects_with_connection( .prepare( "SELECT EXISTS( SELECT 1 - FROM agent_inbox inbox - JOIN agent_inbox_materializations receipt + FROM agent_org_runtime_inbox inbox + JOIN agent_org_runtime_inbox_materializations receipt ON receipt.inbox_id=inbox.id AND receipt.session_id=?2 WHERE inbox.id=?1 AND inbox.org_run_id=?3 AND inbox.read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution WHERE resolution.inbox_id=inbox.id ) )", @@ -330,7 +330,7 @@ pub(super) fn load_and_assess( let run_row: Option<(String, Option, i64)> = conn .query_row( "SELECT status, root_session_id, activation_generation - FROM agent_org_runs WHERE id=?1", + FROM agent_org_runtime_runs WHERE id=?1", params![run_id], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), ) @@ -397,7 +397,7 @@ pub(super) fn load_and_assess( crate::coordination::agent_org_tasks::corrupt_task_row_predicate_sql(); let persisted_task_count: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_org_tasks WHERE org_run_id=?1", + "SELECT COUNT(*) FROM agent_org_runtime_tasks WHERE org_run_id=?1", params![run_id], |row| row.get(0), ) @@ -432,7 +432,7 @@ pub(super) fn load_and_assess( CASE WHEN metadata_json IS NULL OR length(CAST(metadata_json AS BLOB))<={metadata_max} THEN metadata_json ELSE '!' END AS metadata_json - FROM agent_org_tasks WHERE org_run_id=?1 + FROM agent_org_runtime_tasks WHERE org_run_id=?1 ) AS bounded_tasks" ); let ( @@ -457,11 +457,11 @@ pub(super) fn load_and_assess( let unread_inbox_count: i64 = conn .query_row( "SELECT COUNT(*) - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE org_run_id=?1 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id )", params![run_id], |row| row.get(0), @@ -470,7 +470,7 @@ pub(super) fn load_and_assess( let active_intervention_member_ids = { let mut stmt = conn .prepare( - "SELECT DISTINCT member_id FROM agent_member_interventions + "SELECT DISTINCT member_id FROM agent_org_runtime_member_interventions WHERE org_run_id=?1 AND member_id<>'coordinator' AND cleared_at IS NULL AND datetime(resume_after)>datetime(?2) @@ -512,7 +512,7 @@ pub(super) fn load_and_assess( .map_err(|err| err.to_string())?; let pending_formal_materialization_count: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_org_member_materializations + "SELECT COUNT(*) FROM agent_org_runtime_member_materializations WHERE org_run_id=?1 AND generation=?2 AND authority_class IN ('starting', 'formal') AND status<>'succeeded'", @@ -522,7 +522,7 @@ pub(super) fn load_and_assess( .map_err(|err| err.to_string())?; let active_recovery_reservation_count: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_org_recovery_attempts + "SELECT COUNT(*) FROM agent_org_runtime_recovery_attempts WHERE org_run_id=?1 AND reservation_token IS NOT NULL", params![run_id], |row| row.get(0), @@ -530,7 +530,7 @@ pub(super) fn load_and_assess( .map_err(|err| err.to_string())?; let pending_plan_approval_count: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_org_plan_approvals + "SELECT COUNT(*) FROM agent_org_runtime_plan_approvals WHERE org_run_id=?1 AND status='pending'", params![run_id], |row| row.get(0), diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/store.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/store.rs index c6a8b682ed..b8da080faf 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/store.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/store.rs @@ -6,7 +6,9 @@ use crate::coordination::agent_member_interventions::AgentMemberInterventionStor use crate::coordination::agent_org_plan_approvals::AgentOrgPlanApprovalStore; use crate::coordination::agent_org_tasks::{AgentOrgTaskStore, Task, TaskStatus}; use crate::session::SessionStatus; -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use super::helpers::{ context_for_run_record, flatten_members, insert_run, load_by_id, load_by_root_session, @@ -82,7 +84,7 @@ impl AgentOrgRunStore { if root_session_ids.is_empty() { return Ok(Vec::new()); } - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let placeholders = (1..=root_session_ids.len()) .map(|index| format!("?{index}")) .collect::>() @@ -107,7 +109,7 @@ impl AgentOrgRunStore { created_at, updated_at, idled_at - FROM agent_org_runs + FROM agent_org_runtime_runs WHERE root_session_id IN ({placeholders}) ORDER BY updated_at DESC, id DESC" ); @@ -156,7 +158,7 @@ impl AgentOrgRunStore { }; with_sessions_writer(|| -> Result<(), String> { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -259,7 +261,7 @@ impl AgentOrgRunStore { } with_sessions_writer(|| -> Result<(), String> { - let mut connection = get_connection().map_err(|error| error.to_string())?; + let mut connection = runtime_connection().map_err(|error| error.to_string())?; let transaction = connection .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|error| error.to_string())?; @@ -306,24 +308,24 @@ impl AgentOrgRunStore { } pub fn materializations(run_id: &str) -> Result, String> { - let connection = get_connection().map_err(|error| error.to_string())?; + let connection = runtime_connection().map_err(|error| error.to_string())?; list_materializations_with_connection(&connection, run_id) } pub fn initial_input(run_id: &str) -> Result, String> { - let connection = get_connection().map_err(|error| error.to_string())?; + let connection = runtime_connection().map_err(|error| error.to_string())?; load_initial_input_with_connection(&connection, run_id) } pub fn initial_input_for_turn( turn_intent_id: &str, ) -> Result, String> { - let connection = get_connection().map_err(|error| error.to_string())?; + let connection = runtime_connection().map_err(|error| error.to_string())?; load_initial_input_by_turn_with_connection(&connection, turn_intent_id) } pub fn recoverable_initial_inputs(limit: usize) -> Result, String> { - let connection = get_connection().map_err(|error| error.to_string())?; + let connection = runtime_connection().map_err(|error| error.to_string())?; list_recoverable_initial_inputs_with_connection(&connection, limit) } @@ -341,7 +343,7 @@ impl AgentOrgRunStore { session_id: &str, ) -> Result { with_sessions_writer(|| -> Result { - let mut connection = get_connection().map_err(|error| error.to_string())?; + let mut connection = runtime_connection().map_err(|error| error.to_string())?; let transaction = connection .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|error| error.to_string())?; @@ -349,8 +351,8 @@ impl AgentOrgRunStore { .query_row( "SELECT materialization.agent_id, materialization.session_id, run.root_session_id, materialization.status - FROM agent_org_member_materializations materialization - JOIN agent_org_runs run ON run.id=materialization.org_run_id + FROM agent_org_runtime_member_materializations materialization + JOIN agent_org_runtime_runs run ON run.id=materialization.org_run_id WHERE materialization.org_run_id=?1 AND materialization.member_id=?2 AND materialization.generation=?3 @@ -402,7 +404,7 @@ impl AgentOrgRunStore { } let changed = transaction .execute( - "UPDATE agent_org_member_materializations + "UPDATE agent_org_runtime_member_materializations SET status='succeeded', error_code=NULL, error_json=NULL, updated_at=?5 WHERE org_run_id=?1 AND member_id=?2 AND generation=?3 @@ -428,14 +430,14 @@ impl AgentOrgRunStore { expected_generation: i64, ) -> Result { let status = with_sessions_writer(|| -> Result { - let mut connection = get_connection().map_err(|error| error.to_string())?; + let mut connection = runtime_connection().map_err(|error| error.to_string())?; let transaction = connection .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|error| error.to_string())?; let run: Option<(String, String, i64, bool)> = transaction .query_row( "SELECT status, root_session_id, activation_generation, has_initial_work - FROM agent_org_runs WHERE id=?1", + FROM agent_org_runtime_runs WHERE id=?1", [run_id], |row| { Ok(( @@ -465,7 +467,7 @@ impl AgentOrgRunStore { let invalid_materialized_identities: i64 = transaction .query_row( "SELECT COUNT(*) - FROM agent_org_member_materializations materialization + FROM agent_org_runtime_member_materializations materialization LEFT JOIN agent_sessions session ON session.session_id=materialization.session_id WHERE materialization.org_run_id=?1 @@ -509,7 +511,7 @@ impl AgentOrgRunStore { } let incomplete_materializations: i64 = transaction .query_row( - "SELECT COUNT(*) FROM agent_org_member_materializations + "SELECT COUNT(*) FROM agent_org_runtime_member_materializations WHERE org_run_id=?1 AND generation=?2 AND status<>'succeeded'", params![run_id, expected_generation], |row| row.get(0), @@ -559,7 +561,7 @@ impl AgentOrgRunStore { )?; transaction .execute( - "UPDATE agent_org_initial_inputs + "UPDATE agent_org_runtime_initial_inputs SET status='queued', updated_at=?2 WHERE org_run_id=?1 AND status='pending_persistence'", params![run_id, chrono::Utc::now().to_rfc3339()], @@ -579,7 +581,7 @@ impl AgentOrgRunStore { let now = chrono::Utc::now().to_rfc3339(); let changed = transaction .execute( - "UPDATE agent_org_runs + "UPDATE agent_org_runtime_runs SET status=?1, updated_at=?2, idled_at=CASE WHEN ?1='idle' THEN ?2 ELSE NULL END WHERE id=?3 AND status='starting' @@ -602,10 +604,10 @@ impl AgentOrgRunStore { turn_intent_id: &str, ) -> Result { with_sessions_writer(|| { - let connection = get_connection().map_err(|error| error.to_string())?; + let connection = runtime_connection().map_err(|error| error.to_string())?; let changed = connection .execute( - "UPDATE agent_org_initial_inputs + "UPDATE agent_org_runtime_initial_inputs SET status='dispatched', updated_at=?3 WHERE org_run_id=?1 AND turn_intent_id=?2 AND status IN ('queued', 'dispatched')", @@ -627,10 +629,10 @@ impl AgentOrgRunStore { let running = validate_status(AgentOrgRunStatus::Running.as_str())?; let now = chrono::Utc::now().to_rfc3339(); let changed = with_sessions_writer(|| -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let rows_changed = conn .execute( - "UPDATE agent_org_runs + "UPDATE agent_org_runtime_runs SET status = ?1, updated_at = ?2 WHERE id = ?3 @@ -675,10 +677,10 @@ impl AgentOrgRunStore { let paused = validate_status(AgentOrgRunStatus::Paused.as_str())?; let now = chrono::Utc::now().to_rfc3339(); let changed = with_sessions_writer(|| -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let rows_changed = conn .execute( - "UPDATE agent_org_runs + "UPDATE agent_org_runtime_runs SET status = ?1, updated_at = ?2 WHERE id = ?3 @@ -703,13 +705,13 @@ impl AgentOrgRunStore { .map_err(|error| format!("failed to serialize Starting failure: {error}"))?; let now = chrono::Utc::now().to_rfc3339(); let changed = with_sessions_writer(|| -> Result { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; let changed = tx .execute( - "UPDATE agent_org_runs + "UPDATE agent_org_runtime_runs SET status = 'failed', last_error = ?2, failure_json = ?3, @@ -736,7 +738,7 @@ impl AgentOrgRunStore { } pub fn progress(run_id: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; load_progress_with_conn(&conn, run_id) } @@ -745,7 +747,7 @@ impl AgentOrgRunStore { /// revision to `observed`; newer concurrent task mutations remain newer. pub fn stage_coordinator_work_revision(run_id: &str) -> Result, String> { let revision = with_sessions_writer(|| { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; stage_coordinator_presented_with_conn(&conn, run_id) })?; if revision.is_some() { @@ -763,7 +765,7 @@ impl AgentOrgRunStore { ) -> Result<(Option, Vec), String> { let (revision, tasks) = with_sessions_writer(|| -> Result<(Option, Vec), String> { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -783,7 +785,7 @@ impl AgentOrgRunStore { presented_work_revision: i64, ) -> Result, String> { let observed_revision = with_sessions_writer(|| { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; mark_coordinator_observed_revision_with_conn(&conn, run_id, presented_work_revision) })?; if observed_revision.is_some() { @@ -800,13 +802,13 @@ impl AgentOrgRunStore { summary: &str, ) -> Result { let outcome = with_sessions_writer(|| { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; let status: Option = tx .query_row( - "SELECT status FROM agent_org_runs WHERE id=?1", + "SELECT status FROM agent_org_runtime_runs WHERE id=?1", params![run_id], |row| row.get(0), ) @@ -824,7 +826,7 @@ impl AgentOrgRunStore { let unresolved_task_ids = { let mut stmt = tx .prepare( - "SELECT id FROM agent_org_tasks + "SELECT id FROM agent_org_runtime_tasks WHERE org_run_id=?1 AND status<>?2 ORDER BY created_at ASC, id ASC", ) @@ -854,7 +856,7 @@ impl AgentOrgRunStore { } pub fn assess_run_quiescence(run_id: &str) -> Result { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Deferred) .map_err(|err| err.to_string())?; @@ -892,7 +894,7 @@ impl AgentOrgRunStore { expected_work_revision: i64, ) -> Result { let changed = with_sessions_writer(|| -> Result { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -918,7 +920,7 @@ impl AgentOrgRunStore { .and_then(|progress| progress.completion_summary.as_deref()); let changed = tx .execute( - "UPDATE agent_org_runs + "UPDATE agent_org_runtime_runs SET status='idle', summary=COALESCE(?1, summary), last_activity_outcome='completed', @@ -927,8 +929,8 @@ impl AgentOrgRunStore { WHERE id=?3 AND status='running' AND activation_generation=?4 AND EXISTS ( - SELECT 1 FROM agent_org_run_progress progress - WHERE progress.org_run_id=agent_org_runs.id + SELECT 1 FROM agent_org_runtime_run_progress progress + WHERE progress.org_run_id=agent_org_runtime_runs.id AND progress.work_revision=?5 )", params![ @@ -957,7 +959,7 @@ impl AgentOrgRunStore { /// misses, walk the persisted `agent_sessions.parent_session_id` /// chain upward (using the existing `idx_agent_sessions_parent` /// index) and retry the lookup at each ancestor. The first ancestor - /// that anchors an `agent_org_runs` row wins. + /// that anchors an `agent_org_runtime_runs` row wins. /// /// The persisted parent chain serves as the reverse-resolution /// path. `root_session_id` remains the **single anchor** for an org @@ -993,10 +995,10 @@ impl AgentOrgRunStore { } pub fn is_root_session(org_run_id: &str, session_id: &str) -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let root_session_id: Option = conn .query_row( - "SELECT root_session_id FROM agent_org_runs WHERE id = ?1", + "SELECT root_session_id FROM agent_org_runtime_runs WHERE id = ?1", params![org_run_id], |row| row.get::<_, Option>(0), ) @@ -1018,7 +1020,7 @@ impl AgentOrgRunStore { tracing::warn!( session_id = %session_id, cycle_at = %current_id, - "[agent_org_runs] parent_session_id chain has a cycle; aborting walk" + "[agent_org_runtime_runs] parent_session_id chain has a cycle; aborting walk" ); return Ok(None); } @@ -1029,7 +1031,7 @@ impl AgentOrgRunStore { tracing::warn!( session_id = %session_id, last_visited = %current_id, - "[agent_org_runs] parent_session_id walk exceeded max depth ({}); giving up", + "[agent_org_runtime_runs] parent_session_id walk exceeded max depth ({}); giving up", MAX_PARENT_WALK_DEPTH ); return Ok(None); @@ -1051,7 +1053,7 @@ impl AgentOrgRunStore { /// Inbox renders those as transient client-side draft rows until the /// anchor exists. pub fn list_runs(limit: usize) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( "SELECT id, @@ -1073,7 +1075,7 @@ impl AgentOrgRunStore { created_at, updated_at, idled_at - FROM agent_org_runs + FROM agent_org_runtime_runs WHERE root_session_id IS NOT NULL ORDER BY updated_at DESC LIMIT ?1", @@ -1111,7 +1113,7 @@ impl AgentOrgRunStore { } let bounded_limit = i64::try_from(limit) .map_err(|_| format!("Agent Org run list limit is too large: {limit}"))?; - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( "SELECT id, @@ -1133,7 +1135,7 @@ impl AgentOrgRunStore { created_at, updated_at, idled_at - FROM agent_org_runs + FROM agent_org_runtime_runs WHERE root_session_id IS NOT NULL AND status = ?1 AND (updated_at > ?2 OR (updated_at = ?2 AND id > ?3)) @@ -1165,7 +1167,7 @@ impl AgentOrgRunStore { } let bounded_limit = i64::try_from(limit) .map_err(|_| format!("Agent Org run list limit is too large: {limit}"))?; - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( "SELECT id, @@ -1187,7 +1189,7 @@ impl AgentOrgRunStore { created_at, updated_at, idled_at - FROM agent_org_runs + FROM agent_org_runtime_runs WHERE root_session_id IS NOT NULL AND status = ?1 ORDER BY updated_at ASC, id ASC @@ -1206,7 +1208,7 @@ impl AgentOrgRunStore { /// Return the current status of the run without fetching the full record. pub fn get_run_status(run_id: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::get_run_status_with_connection(&conn, run_id) } @@ -1216,7 +1218,7 @@ impl AgentOrgRunStore { ) -> Result, String> { let status_raw: Option = conn .query_row( - "SELECT status FROM agent_org_runs WHERE id = ?1 LIMIT 1", + "SELECT status FROM agent_org_runtime_runs WHERE id = ?1 LIMIT 1", params![run_id], |row| row.get(0), ) @@ -1238,7 +1240,7 @@ impl AgentOrgRunStore { pub fn delete_by_id(run_id: &str) -> Result<(), String> { let outcome = with_sessions_writer(|| -> Result { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -1259,10 +1261,10 @@ impl AgentOrgRunStore { let mut stmt = conn .prepare( "SELECT DISTINCT approval.source_session_id, approval.plan_path - FROM agent_org_plan_approvals approval + FROM agent_org_runtime_plan_approvals approval WHERE approval.org_run_id=?1 AND NOT EXISTS ( - SELECT 1 FROM agent_org_plan_approvals other + SELECT 1 FROM agent_org_runtime_plan_approvals other WHERE other.plan_path=approval.plan_path AND other.org_run_id<>?1 )", @@ -1286,25 +1288,25 @@ impl AgentOrgRunStore { ) .map_err(|err| err.to_string())?; conn.execute( - "DELETE FROM agent_inbox_materializations + "DELETE FROM agent_org_runtime_inbox_materializations WHERE inbox_id IN ( - SELECT id FROM agent_inbox WHERE org_run_id=?1 + SELECT id FROM agent_org_runtime_inbox WHERE org_run_id=?1 )", params![run_id], ) .map_err(|err| { - format!("failed to delete agent_inbox_materializations rows for {run_id}: {err}") + format!("failed to delete agent_org_runtime_inbox_materializations rows for {run_id}: {err}") })?; for table in [ - "agent_org_plan_approvals", - "agent_org_recovery_attempts", - "agent_org_task_events", - "agent_org_tasks", - "agent_inbox_delivery_resolutions", - "agent_inbox", - "agent_member_interventions", - "agent_org_run_progress", - "agent_org_task_run_schema_migrations", + "agent_org_runtime_plan_approvals", + "agent_org_runtime_recovery_attempts", + "agent_org_runtime_task_events", + "agent_org_runtime_tasks", + "agent_org_runtime_inbox_delivery_resolutions", + "agent_org_runtime_inbox", + "agent_org_runtime_member_interventions", + "agent_org_runtime_run_progress", + "agent_org_runtime_task_schema_migrations", ] { conn.execute( &format!("DELETE FROM {table} WHERE org_run_id=?1"), @@ -1313,7 +1315,10 @@ impl AgentOrgRunStore { .map_err(|err| format!("failed to delete {table} rows for {run_id}: {err}"))?; } let deleted = conn - .execute("DELETE FROM agent_org_runs WHERE id=?1", params![run_id]) + .execute( + "DELETE FROM agent_org_runtime_runs WHERE id=?1", + params![run_id], + ) .map_err(|err| err.to_string())? > 0; Ok(AgentOrgRunDeleteOutcome { @@ -1364,7 +1369,7 @@ impl AgentOrgRunStore { org_run_id: &str, member_id: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::find_coordinator_session_by_member_id_with_connection(&conn, org_run_id, member_id) } @@ -1381,7 +1386,7 @@ impl AgentOrgRunStore { "SELECT s.session_id, s.status, s.updated_at - FROM agent_org_runs r + FROM agent_org_runtime_runs r JOIN agent_sessions s ON s.session_id = r.root_session_id WHERE r.id = ?1 LIMIT 1", @@ -1411,7 +1416,7 @@ impl AgentOrgRunStore { org_run_id: &str, member_ids: &[String], ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::list_worker_sessions_by_member_ids_with_connection(&conn, org_run_id, member_ids) } @@ -1455,7 +1460,7 @@ impl AgentOrgRunStore { ) -> Result>, String> { let snapshot_json: Option = conn .query_row( - "SELECT org_snapshot_json FROM agent_org_runs WHERE id=?1", + "SELECT org_snapshot_json FROM agent_org_runtime_runs WHERE id=?1", params![org_run_id], |row| row.get(0), ) @@ -1483,7 +1488,7 @@ impl AgentOrgRunStore { pub fn list_descendant_worker_sessions( org_run_id: &str, ) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::list_descendant_worker_sessions_with_connection(&conn, org_run_id) } @@ -1493,7 +1498,7 @@ impl AgentOrgRunStore { ) -> Result, String> { let root_session_id: Option = conn .query_row( - "SELECT root_session_id FROM agent_org_runs WHERE id = ?1", + "SELECT root_session_id FROM agent_org_runtime_runs WHERE id = ?1", params![org_run_id], |row| row.get::<_, Option>(0), ) @@ -1516,7 +1521,7 @@ impl AgentOrgRunStore { FROM agent_sessions child WHERE child.parent_session_id = ?1 AND NOT EXISTS ( - SELECT 1 FROM agent_org_runs nested + SELECT 1 FROM agent_org_runtime_runs nested WHERE nested.id <> ?2 AND nested.root_session_id = child.session_id ) @@ -1525,7 +1530,7 @@ impl AgentOrgRunStore { FROM agent_sessions s JOIN descendants d ON s.parent_session_id = d.session_id WHERE NOT EXISTS ( - SELECT 1 FROM agent_org_runs nested + SELECT 1 FROM agent_org_runtime_runs nested WHERE nested.id <> ?2 AND nested.root_session_id = s.session_id ) diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/tests.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/tests.rs index 8692d923d4..a40a3e4185 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/tests.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_runs/tests.rs @@ -3,67 +3,7 @@ use super::*; use crate::core::session::persistence::{upsert_session, UnifiedSessionRecord}; use crate::core::session::SessionStatus; use crate::definitions::orgs::{AgentOrgsStore, FlatOrgMember, OrgDefinition, PlanApprovalPolicy}; -use rusqlite::{params, Connection}; - -const LEGACY_AGENT_ORG_RUNS_DDL: &str = "CREATE TABLE agent_org_runs ( - id TEXT PRIMARY KEY, - org_id TEXT NOT NULL, - coordinator_agent_id TEXT NOT NULL, - root_session_id TEXT, - org_snapshot_json TEXT, - entry_mode TEXT NOT NULL, - status TEXT NOT NULL, - work_item_id TEXT, - project_slug TEXT, - routine_fire_id TEXT, - summary TEXT, - last_error TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - completed_at TEXT -);"; - -fn memory_connection() -> Connection { - let conn = Connection::open_in_memory().expect("in-memory sqlite"); - conn.execute_batch("PRAGMA foreign_keys = ON;") - .expect("enable foreign keys"); - conn -} - -fn insert_legacy_run(conn: &Connection) { - conn.execute( - "INSERT INTO agent_org_runs ( - id, org_id, coordinator_agent_id, root_session_id, - org_snapshot_json, entry_mode, status, summary, last_error, - created_at, updated_at - ) VALUES ( - 'legacy-run', 'legacy-org', 'legacy-coordinator', 'legacy-root', - '{}', 'standalone_session', 'running', 'legacy summary', 'legacy error', - '2026-01-01T00:00:00Z', '2026-01-02T00:00:00Z' - )", - [], - ) - .expect("insert legacy run sentinel"); -} - -fn row_count(conn: &Connection, table: &str) -> i64 { - conn.query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |row| { - row.get(0) - }) - .unwrap_or_else(|error| panic!("count {table}: {error}")) -} - -fn index_names(conn: &Connection) -> Vec { - conn.prepare( - "SELECT name FROM sqlite_master - WHERE type='index' AND name LIKE 'idx_agent_org_%' ORDER BY name", - ) - .expect("prepare index query") - .query_map([], |row| row.get(0)) - .expect("query indexes") - .collect::>>() - .expect("collect indexes") -} +use rusqlite::params; #[test] fn enum_values_round_trip() { @@ -94,7 +34,7 @@ fn canonical_schema_snapshot_contains_only_the_long_lived_run_states() { let run_ddl: String = conn .query_row( "SELECT sql FROM sqlite_master - WHERE type='table' AND name='agent_org_runs'", + WHERE type='table' AND name='agent_org_runtime_runs'", [], |row| row.get(0), ) @@ -119,7 +59,7 @@ fn canonical_schema_snapshot_contains_only_the_long_lived_run_states() { let materialization_ddl: String = conn .query_row( "SELECT sql FROM sqlite_master - WHERE type='table' AND name='agent_org_member_materializations'", + WHERE type='table' AND name='agent_org_runtime_member_materializations'", [], |row| row.get(0), ) @@ -130,7 +70,7 @@ fn canonical_schema_snapshot_contains_only_the_long_lived_run_states() { let initial_input_ddl: String = conn .query_row( "SELECT sql FROM sqlite_master - WHERE type='table' AND name='agent_org_initial_inputs'", + WHERE type='table' AND name='agent_org_runtime_initial_inputs'", [], |row| row.get(0), ) @@ -139,201 +79,6 @@ fn canonical_schema_snapshot_contains_only_the_long_lived_run_states() { assert!(initial_input_ddl.contains("UNIQUE(message_id)")); } -#[test] -fn exact_legacy_run_schema_resets_only_the_agent_org_runtime_envelope() { - let conn = memory_connection(); - conn.execute_batch(LEGACY_AGENT_ORG_RUNS_DDL) - .expect("legacy run schema"); - insert_legacy_run(&conn); - materialization::init_schema(&conn).expect("materialization schema from bad binary"); - progress::init_schema(&conn).expect("legacy progress schema"); - conn.execute_batch( - "UPDATE agent_org_run_progress SET work_revision=7 WHERE org_run_id='legacy-run'; - INSERT INTO agent_org_member_materializations ( - org_run_id, member_id, agent_id, generation, session_id, - authority_class, status, created_at, updated_at - ) VALUES ( - 'legacy-run', 'legacy-member', 'legacy-agent', 1, 'legacy-member-session', - 'starting', 'succeeded', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z' - ); - INSERT INTO agent_org_initial_inputs ( - org_run_id, turn_intent_id, message_id, content, payload_json, - status, created_at, updated_at - ) VALUES ( - 'legacy-run', 'legacy-turn', 'legacy-message', 'legacy input', '{}', - 'dispatched', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z' - ); - CREATE TABLE agent_sessions ( - session_id TEXT PRIMARY KEY, title TEXT, status TEXT, updated_at TEXT - ); - CREATE TABLE code_sessions ( - session_id TEXT PRIMARY KEY, cli_agent_type TEXT, status TEXT, updated_at TEXT - ); - INSERT INTO agent_sessions VALUES ( - 'rust-sentinel', 'Rust sentinel', 'idle', '2025-12-01T00:00:00Z' - ); - INSERT INTO code_sessions VALUES ( - 'cli-sentinel', 'codex', 'completed', '2025-12-02T00:00:00Z' - );", - ) - .expect("legacy runtime and ordinary session sentinels"); - - init_schema(&conn).expect("reset exact legacy schema"); - - for table in [ - "agent_org_initial_inputs", - "agent_org_member_materializations", - "agent_org_run_progress", - "agent_org_runs", - ] { - assert_eq!(row_count(&conn, table), 0, "{table} must be reset"); - } - let new_column_count: i64 = conn - .query_row( - "SELECT COUNT(*) FROM pragma_table_info('agent_org_runs') - WHERE name IN ('activation_generation', 'has_initial_work', 'failure_json', - 'last_activity_outcome', 'idled_at')", - [], - |row| row.get(0), - ) - .expect("canonical run columns"); - assert_eq!(new_column_count, 5); - let run_ddl: String = conn - .query_row( - "SELECT sql FROM sqlite_master WHERE type='table' AND name='agent_org_runs'", - [], - |row| row.get(0), - ) - .expect("canonical run DDL"); - assert!(run_ddl.contains("'starting', 'running', 'paused', 'idle', 'failed', 'archived'")); - assert!(!run_ddl.contains("completed_at")); - let rust_unchanged: i64 = conn - .query_row( - "SELECT COUNT(*) FROM agent_sessions WHERE session_id='rust-sentinel' - AND title='Rust sentinel' AND status='idle' AND updated_at='2025-12-01T00:00:00Z'", - [], - |row| row.get(0), - ) - .expect("Rust session sentinel"); - let cli_unchanged: i64 = conn - .query_row( - "SELECT COUNT(*) FROM code_sessions WHERE session_id='cli-sentinel' - AND cli_agent_type='codex' AND status='completed' - AND updated_at='2025-12-02T00:00:00Z'", - [], - |row| row.get(0), - ) - .expect("CLI session sentinel"); - assert_eq!((rust_unchanged, cli_unchanged), (1, 1)); - - conn.execute( - "INSERT INTO agent_org_runs ( - id, org_id, coordinator_agent_id, entry_mode, status, created_at, updated_at - ) VALUES ( - 'new-run', 'new-org', 'new-coordinator', 'standalone_session', 'starting', - '2026-02-01T00:00:00Z', '2026-02-01T00:00:00Z' - )", - [], - ) - .expect("insert canonical starting run"); - let defaults: (String, i64, i64) = conn - .query_row( - "SELECT status, activation_generation, has_initial_work - FROM agent_org_runs WHERE id='new-run'", - [], - |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), - ) - .expect("read canonical starting run"); - assert_eq!(defaults, ("starting".into(), 1, 0)); -} - -#[test] -fn canonical_schema_init_is_idempotent_and_preserves_runtime_data() { - let conn = memory_connection(); - init_schema(&conn).expect("create canonical schema"); - conn.execute_batch( - "INSERT INTO agent_org_runs ( - id, org_id, coordinator_agent_id, entry_mode, status, created_at, updated_at - ) VALUES ( - 'current-run', 'current-org', 'current-coordinator', - 'standalone_session', 'starting', '2026-03-01T00:00:00Z', '2026-03-01T00:00:00Z' - ); - INSERT INTO agent_org_run_progress ( - org_run_id, work_revision, completion_requested, completion_summary, updated_at - ) VALUES ('current-run', 9, 1, 'done', '2026-03-01T00:00:00Z'); - INSERT INTO agent_org_member_materializations ( - org_run_id, member_id, agent_id, generation, session_id, - authority_class, status, created_at, updated_at - ) VALUES ( - 'current-run', 'member-a', 'agent-a', 1, 'session-a', 'starting', 'succeeded', - '2026-03-01T00:00:00Z', '2026-03-01T00:00:00Z' - ); - INSERT INTO agent_org_initial_inputs ( - org_run_id, turn_intent_id, message_id, content, payload_json, - status, created_at, updated_at - ) VALUES ( - 'current-run', 'turn-a', 'message-a', 'hello', '{}', 'queued', - '2026-03-01T00:00:00Z', '2026-03-01T00:00:00Z' - );", - ) - .expect("canonical runtime fixtures"); - let indexes_before = index_names(&conn); - - init_schema(&conn).expect("repeat canonical init"); - - assert_eq!(index_names(&conn), indexes_before); - assert_eq!(row_count(&conn, "agent_org_runs"), 1); - assert_eq!(row_count(&conn, "agent_org_run_progress"), 1); - assert_eq!(row_count(&conn, "agent_org_member_materializations"), 1); - assert_eq!(row_count(&conn, "agent_org_initial_inputs"), 1); - let preserved: i64 = conn - .query_row( - "SELECT COUNT(*) - FROM agent_org_run_progress progress - JOIN agent_org_member_materializations materialization - ON materialization.org_run_id=progress.org_run_id - JOIN agent_org_initial_inputs input ON input.org_run_id=progress.org_run_id - WHERE progress.work_revision=9 AND progress.completion_summary='done' - AND materialization.session_id='session-a' AND input.content='hello'", - [], - |row| row.get(0), - ) - .expect("preserved canonical data"); - assert_eq!(preserved, 1); -} - -#[test] -fn unknown_run_schemas_never_trigger_destructive_reset() { - for ddl in [ - LEGACY_AGENT_ORG_RUNS_DDL.replace( - "completed_at TEXT\n);", - "completed_at TEXT, unknown_column TEXT\n);", - ), - LEGACY_AGENT_ORG_RUNS_DDL.replace("org_id TEXT NOT NULL", "org_id BLOB NOT NULL"), - LEGACY_AGENT_ORG_RUNS_DDL - .replace("root_session_id TEXT,", "root_session_id TEXT NOT NULL,"), - LEGACY_AGENT_ORG_RUNS_DDL.replace("summary TEXT,", "summary TEXT DEFAULT 'legacy',"), - LEGACY_AGENT_ORG_RUNS_DDL.replace("id TEXT PRIMARY KEY", "id TEXT"), - ] { - let conn = memory_connection(); - conn.execute_batch(&ddl) - .expect("create unknown schema fixture"); - insert_legacy_run(&conn); - - let _ = init_schema(&conn); - - assert_eq!(row_count(&conn, "agent_org_runs"), 1); - let sentinel: String = conn - .query_row( - "SELECT summary FROM agent_org_runs WHERE id='legacy-run'", - [], - |row| row.get(0), - ) - .expect("read unknown schema sentinel"); - assert_eq!(sentinel, "legacy summary"); - } -} - /// Build an `AgentOrgsStore` pre-loaded with a single org definition. /// Bypasses the disk loader so tests stay hermetic — the sandbox /// already isolates `~/.orgii`, but we don't need to touch disk at @@ -746,7 +491,7 @@ fn delete_by_id_cascades_all_run_owned_state_and_plan_artifact() { .expect("attach managed workspace to source session"); let now = chrono::Utc::now().to_rfc3339(); conn.execute( - "INSERT INTO agent_org_plan_approvals ( + "INSERT INTO agent_org_runtime_plan_approvals ( approval_id, plan_revision_id, request_id, org_run_id, source_task_id, source_member_id, source_session_id, root_session_id, policy, status, plan_title, plan_path, @@ -759,7 +504,7 @@ fn delete_by_id_cascades_all_run_owned_state_and_plan_artifact() { ) .unwrap(); conn.execute( - "INSERT INTO agent_org_plan_approvals ( + "INSERT INTO agent_org_runtime_plan_approvals ( approval_id, plan_revision_id, request_id, org_run_id, source_task_id, source_member_id, source_session_id, root_session_id, policy, status, plan_title, plan_path, @@ -772,7 +517,7 @@ fn delete_by_id_cascades_all_run_owned_state_and_plan_artifact() { ) .unwrap(); conn.execute( - "INSERT INTO agent_org_recovery_attempts + "INSERT INTO agent_org_runtime_recovery_attempts (org_run_id, action_kind, target_key, reason_fingerprint, attempts, next_allowed_at, updated_at) VALUES (?1,'member_rewake','member-w1','delete',1,?2,?2)", @@ -780,7 +525,7 @@ fn delete_by_id_cascades_all_run_owned_state_and_plan_artifact() { ) .unwrap(); conn.execute( - "INSERT INTO agent_org_task_run_schema_migrations + "INSERT INTO agent_org_runtime_task_schema_migrations (name, org_run_id, applied_at) VALUES ('delete-test', ?1, ?2)", params![&run.id, &now], @@ -797,14 +542,14 @@ fn delete_by_id_cascades_all_run_owned_state_and_plan_artifact() { AgentOrgRunStore::delete_by_id(&run.id).expect("delete run-owned state"); for table in [ - "agent_org_run_progress", - "agent_org_tasks", - "agent_org_task_events", - "agent_inbox", - "agent_member_interventions", - "agent_org_plan_approvals", - "agent_org_recovery_attempts", - "agent_org_task_run_schema_migrations", + "agent_org_runtime_run_progress", + "agent_org_runtime_tasks", + "agent_org_runtime_task_events", + "agent_org_runtime_inbox", + "agent_org_runtime_member_interventions", + "agent_org_runtime_plan_approvals", + "agent_org_runtime_recovery_attempts", + "agent_org_runtime_task_schema_migrations", ] { let count: i64 = conn .query_row( @@ -1569,14 +1314,14 @@ fn quiescence_transitions_run_to_idle_when_all_tasks_completed() { "a {terminal_status} turn intent must not keep the run open" ); conn.execute( - "UPDATE agent_org_runs SET status='running', idled_at=NULL WHERE id=?1", + "UPDATE agent_org_runtime_runs SET status='running', idled_at=NULL WHERE id=?1", params![&run.id], ) .expect("reset run for next terminal status"); } let legacy_resume_after = (chrono::Utc::now() + chrono::Duration::minutes(3)).to_rfc3339(); conn.execute( - "INSERT INTO agent_member_interventions ( + "INSERT INTO agent_org_runtime_member_interventions ( org_run_id, member_id, agent_id, session_id, status, reason, entered_at, last_user_activity_at, resume_after, cleared_at ) VALUES (?1, ?2, 'agent-coord', ?3, 'user_intervention', @@ -1605,7 +1350,7 @@ fn quiescence_transitions_run_to_idle_when_all_tasks_completed() { assert!(reloaded.idled_at.is_some()); let legacy_cleared_at: Option = conn .query_row( - "SELECT cleared_at FROM agent_member_interventions + "SELECT cleared_at FROM agent_org_runtime_member_interventions WHERE org_run_id=?1 AND member_id=?2", params![&run.id, COORDINATOR_MEMBER_ID], |row| row.get(0), @@ -1729,7 +1474,7 @@ fn resolved_undeliverable_inbox_stays_unread_but_no_longer_blocks_quiescence() { }; let conn = database::db::get_connection().expect("test sqlite connection"); conn.execute( - "INSERT INTO agent_inbox ( + "INSERT INTO agent_org_runtime_inbox ( recipient_agent_id, recipient_member_id, sender_agent_id, sender_member_id, org_run_id, payload_kind, payload_json, created_at diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/helpers.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/helpers.rs index f8b5a2ff4c..bb650ab021 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/helpers.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/helpers.rs @@ -138,7 +138,7 @@ pub(super) fn insert_task_history_event( actor_member_id: Option<&str>, ) -> Result<(), String> { tx.execute( - "INSERT INTO agent_org_task_events ( + "INSERT INTO agent_org_runtime_task_events ( id, org_run_id, task_id, event_type, previous_owner, next_owner, previous_status, next_status, actor_member_id, created_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", @@ -164,7 +164,7 @@ pub(super) fn list_tasks_with_conn( org_run_id: &str, ) -> Result, String> { let sql = format!( - "SELECT {SELECT_COLUMNS} FROM agent_org_tasks + "SELECT {SELECT_COLUMNS} FROM agent_org_runtime_tasks WHERE org_run_id = ?1 ORDER BY created_at ASC, id ASC" ); diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/mod.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/mod.rs index 6f8e309917..ae616d415f 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/mod.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/mod.rs @@ -1,6 +1,6 @@ //! Agent Org task store: Task schema, persisted to SQLite. //! -//! Tasks are stored in a single `agent_org_tasks` table scoped by +//! Tasks are stored in a single `agent_org_runtime_tasks` table scoped by //! `org_run_id` (one Agent Org run = one team execution). //! //! This module exposes the schema, structs, and store CRUD used by the Agent @@ -9,7 +9,9 @@ use std::collections::{HashMap, HashSet}; -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use rusqlite::{params, Connection, Result as SqliteResult}; use crate::coordination::agent_org_runs::{ @@ -646,15 +648,24 @@ pub fn new_task_id() -> String { uuid::Uuid::new_v4().to_string() } -/// Initialize the `agent_org_tasks` table. +/// Initialize the `agent_org_runtime_tasks` table. +/// +/// Tests-only convenience: production initialization goes through the +/// namespace coordinator (`coordination::schema::initialize`), never this +/// module-level entry point. /// /// Hot-path indexes: /// - `(org_run_id, status, owner)` -- bounded status/owner summaries and /// recovery diagnostics. /// - `(org_run_id, owner)` -- per-member listings and failure requeue. pub fn init_schema(conn: &Connection) -> SqliteResult<()> { + create_schema(conn)?; + store::normalize_legacy_dependency_rows(conn) +} + +pub(crate) fn create_schema(conn: &Connection) -> SqliteResult<()> { conn.execute_batch( - "CREATE TABLE IF NOT EXISTS agent_org_tasks ( + "CREATE TABLE IF NOT EXISTS agent_org_runtime_tasks ( id TEXT NOT NULL, org_run_id TEXT NOT NULL, subject TEXT NOT NULL, @@ -669,11 +680,11 @@ pub fn init_schema(conn: &Connection) -> SqliteResult<()> { updated_at TEXT NOT NULL, PRIMARY KEY (org_run_id, id) ); - CREATE INDEX IF NOT EXISTS idx_agent_org_tasks_status - ON agent_org_tasks(org_run_id, status, owner); - CREATE INDEX IF NOT EXISTS idx_agent_org_tasks_owner - ON agent_org_tasks(org_run_id, owner); - CREATE TABLE IF NOT EXISTS agent_org_task_events ( + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_tasks_status + ON agent_org_runtime_tasks(org_run_id, status, owner); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_tasks_owner + ON agent_org_runtime_tasks(org_run_id, owner); + CREATE TABLE IF NOT EXISTS agent_org_runtime_task_events ( id TEXT PRIMARY KEY, org_run_id TEXT NOT NULL, task_id TEXT NOT NULL, @@ -685,47 +696,21 @@ pub fn init_schema(conn: &Connection) -> SqliteResult<()> { actor_member_id TEXT, created_at TEXT NOT NULL ); - CREATE INDEX IF NOT EXISTS idx_agent_org_task_events_run - ON agent_org_task_events(org_run_id, created_at, id); - CREATE INDEX IF NOT EXISTS idx_agent_org_task_events_task - ON agent_org_task_events(org_run_id, task_id, created_at, id);", - )?; - add_column_if_missing(conn, "agent_org_tasks", "active_form", "TEXT")?; - add_column_if_missing( - conn, - "agent_org_tasks", - "blocks_json", - "TEXT NOT NULL DEFAULT '[]'", - )?; - add_column_if_missing( - conn, - "agent_org_tasks", - "blocked_by_json", - "TEXT NOT NULL DEFAULT '[]'", - )?; - add_column_if_missing(conn, "agent_org_tasks", "metadata_json", "TEXT")?; - add_column_if_missing(conn, "agent_org_task_events", "actor_member_id", "TEXT")?; - store::normalize_legacy_dependency_rows(conn)?; - Ok(()) + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_task_events_run + ON agent_org_runtime_task_events(org_run_id, created_at, id); + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_task_events_task + ON agent_org_runtime_task_events(org_run_id, task_id, created_at, id); + CREATE TABLE IF NOT EXISTS agent_org_runtime_task_schema_migrations ( + name TEXT NOT NULL, + org_run_id TEXT NOT NULL, + applied_at TEXT NOT NULL, + PRIMARY KEY (name, org_run_id) + );", + ) } -fn add_column_if_missing( - conn: &Connection, - table_name: &str, - column_name: &str, - column_definition: &str, -) -> SqliteResult<()> { - let sql = format!("ALTER TABLE {table_name} ADD COLUMN {column_name} {column_definition}"); - match conn.execute(&sql, []) { - Ok(_) => Ok(()), - Err(rusqlite::Error::SqliteFailure(err, Some(message))) - if err.code == rusqlite::ErrorCode::Unknown - && message.contains("duplicate column name") => - { - Ok(()) - } - Err(err) => Err(err), - } +pub(crate) fn normalize_runtime_data(conn: &Connection) -> SqliteResult<()> { + store::normalize_legacy_dependency_rows(conn) } /// Inbox helper: enqueue a `TaskAssigned` payload into the task owner's @@ -815,14 +800,14 @@ pub(crate) fn enqueue_task_assignments_if_still_ready_for_recovery( return Ok(Vec::new()); } with_sessions_writer(|| -> Result, String> { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; let running: bool = tx .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_runs WHERE id=?1 AND status='running' + SELECT 1 FROM agent_org_runtime_runs WHERE id=?1 AND status='running' )", params![org_run_id], |row| row.get(0), @@ -872,14 +857,14 @@ pub(crate) fn enqueue_task_assignments_if_still_ready_for_recovery( AND json_type(payload_json, '$.task_id')='text' THEN json_extract(payload_json, '$.task_id') END - FROM agent_inbox INDEXED BY idx_agent_inbox_run_unread_recipient + FROM agent_org_runtime_inbox INDEXED BY idx_agent_org_runtime_inbox_run_unread_recipient WHERE org_run_id=?1 AND recipient_member_id=?2 AND payload_kind='task_assigned' AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id ) ORDER BY id ASC", ) diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/create.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/create.rs index 3653468232..c43f68ab8b 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/create.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/create.rs @@ -4,7 +4,9 @@ use std::collections::HashSet; -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use rusqlite::params; use crate::coordination::agent_org_payload_limits::{ @@ -89,7 +91,7 @@ impl AgentOrgTaskStore { let now = now_rfc3339(); let (task, effect) = with_sessions_writer(|| -> Result<(Task, T), String> { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -146,7 +148,7 @@ impl AgentOrgTaskStore { let blocked_by_json = encode_json_array(&task.blocked_by)?; tx.execute( - "INSERT INTO agent_org_tasks ( + "INSERT INTO agent_org_runtime_tasks ( id, org_run_id, subject, description, active_form, owner, status, blocks_json, blocked_by_json, metadata_json, created_at, updated_at @@ -235,7 +237,7 @@ impl AgentOrgTaskStore { } let (tasks, effect) = with_sessions_writer(|| -> Result<(Vec, T), String> { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -327,7 +329,7 @@ impl AgentOrgTaskStore { let blocked_by_json = encode_json_array(&task.blocked_by)?; let metadata_json = encode_metadata(task.metadata.as_ref())?; tx.execute( - "INSERT INTO agent_org_tasks ( + "INSERT INTO agent_org_runtime_tasks ( id, org_run_id, subject, description, active_form, owner, status, blocks_json, blocked_by_json, metadata_json, created_at, updated_at diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/delete.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/delete.rs index c9a1077f96..74a3691116 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/delete.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/delete.rs @@ -2,7 +2,9 @@ //! `blocked_by`, and (optionally) only applies to the exact inspected row //! version to close the check-then-write race. -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use rusqlite::params; use super::super::helpers::{insert_task_history_event, list_tasks_with_conn, now_rfc3339}; @@ -43,7 +45,7 @@ impl AgentOrgTaskStore { task_id: &str, expected_updated_at: Option<&str>, ) -> Result { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -80,7 +82,7 @@ impl AgentOrgTaskStore { .query_row( "SELECT EXISTS( SELECT 1 - FROM agent_inbox_delivery_resolutions + FROM agent_org_runtime_inbox_delivery_resolutions WHERE org_run_id=?1 AND replacement_task_id=?2 )", params![org_run_id, task_id], @@ -94,7 +96,7 @@ impl AgentOrgTaskStore { } let n = tx .execute( - "DELETE FROM agent_org_tasks WHERE org_run_id = ?1 AND id = ?2", + "DELETE FROM agent_org_runtime_tasks WHERE org_run_id = ?1 AND id = ?2", params![org_run_id, task_id], ) .map_err(|err| err.to_string())?; diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/dependencies.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/dependencies.rs index ef2fc8590a..34e84b6bbf 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/dependencies.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/dependencies.rs @@ -37,7 +37,7 @@ pub(super) fn persist_dependency_projection( ) -> Result<(), String> { let mut stmt = conn .prepare( - "UPDATE agent_org_tasks + "UPDATE agent_org_runtime_tasks SET blocks_json=?1, blocked_by_json=?2 WHERE org_run_id=?3 AND id=?4 AND (blocks_json<>?1 OR blocked_by_json<>?2)", @@ -57,41 +57,47 @@ pub(super) fn persist_dependency_projection( Ok(()) } +const MIGRATION_NAME: &str = "canonical_blocked_by_v1"; +/// Terminal marker for runs whose historical rows can never be normalized +/// safely. Written once (with a single warn) so subsequent boots stop +/// rediscovering and re-warning about the same run. Repairing such a run +/// happens through the runtime repair surfaces, not this boot migration. +const MIGRATION_SKIPPED_NAME: &str = "canonical_blocked_by_v1_skipped"; + /// One-time migration for the historical dual-write dependency fields. /// Legacy `blocks`-only edges are folded into canonical `blocked_by`, then /// both stored columns are rewritten as a consistent forward/reverse pair. +/// +/// Discovery is driven from `agent_org_runtime_runs` (small, one row per +/// run) anti-joined to the per-run migration markers — never from a scan +/// of all task rows. A canonical no-op boot therefore costs exactly one +/// SELECT over the runs table regardless of stored data volume. pub(super) fn normalize_legacy_dependency_rows( conn: &rusqlite::Connection, ) -> rusqlite::Result<()> { - const MIGRATION_NAME: &str = "canonical_blocked_by_v1"; - conn.execute_batch( - "CREATE TABLE IF NOT EXISTS agent_org_task_run_schema_migrations ( - name TEXT NOT NULL, - org_run_id TEXT NOT NULL, - applied_at TEXT NOT NULL, - PRIMARY KEY (name, org_run_id) - );", - )?; - let mut after_run_id: Option = None; loop { let run_ids = { let mut stmt = conn.prepare( - "SELECT task.org_run_id - FROM agent_org_tasks task + "SELECT run.id + FROM agent_org_runtime_runs run WHERE NOT EXISTS ( - SELECT 1 FROM agent_org_task_run_schema_migrations migration - WHERE migration.name=?1 - AND migration.org_run_id=task.org_run_id + SELECT 1 FROM agent_org_runtime_task_schema_migrations migration + WHERE migration.name IN (?1, ?2) + AND migration.org_run_id=run.id ) - AND (?2 IS NULL OR task.org_run_id>?2) - GROUP BY task.org_run_id - ORDER BY task.org_run_id + AND (?3 IS NULL OR run.id>?3) + ORDER BY run.id LIMIT 256", )?; - let rows = stmt.query_map(params![MIGRATION_NAME, after_run_id.as_deref()], |row| { - row.get::<_, String>(0) - })?; + let rows = stmt.query_map( + params![ + MIGRATION_NAME, + MIGRATION_SKIPPED_NAME, + after_run_id.as_deref() + ], + |row| row.get::<_, String>(0), + )?; rows.collect::, _>>()? }; if run_ids.is_empty() { @@ -102,17 +108,17 @@ pub(super) fn normalize_legacy_dependency_rows( for run_id in run_ids { conn.execute_batch("BEGIN IMMEDIATE")?; let normalized = (|| -> Result<(), String> { - let already_applied: bool = conn + let already_resolved: bool = conn .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_task_run_schema_migrations - WHERE name=?1 AND org_run_id=?2 + SELECT 1 FROM agent_org_runtime_task_schema_migrations + WHERE name IN (?1, ?2) AND org_run_id=?3 )", - params![MIGRATION_NAME, &run_id], + params![MIGRATION_NAME, MIGRATION_SKIPPED_NAME, &run_id], |row| row.get(0), ) .map_err(|err| err.to_string())?; - if already_applied { + if already_resolved { return Ok(()); } @@ -127,7 +133,7 @@ pub(super) fn normalize_legacy_dependency_rows( canonicalize_dependencies(&mut tasks, &run_id)?; persist_dependency_projection(conn, &tasks)?; conn.execute( - "INSERT INTO agent_org_task_run_schema_migrations( + "INSERT INTO agent_org_runtime_task_schema_migrations( name, org_run_id, applied_at ) VALUES (?1, ?2, ?3)", params![MIGRATION_NAME, &run_id, now_rfc3339()], @@ -140,11 +146,20 @@ pub(super) fn normalize_legacy_dependency_rows( Ok(()) => conn.execute_batch("COMMIT")?, Err(error) => { let _ = conn.execute_batch("ROLLBACK"); + // Warn exactly once, then mark the run terminally + // skipped so the next boot neither rediscovers nor + // re-warns about it. warn!( org_run_id = %run_id, error = %error, - "deferring corrupt Agent Org task board dependency normalization" + "permanently skipping Agent Org task board dependency normalization; the run requires repair through the runtime repair surfaces" ); + conn.execute( + "INSERT OR IGNORE INTO agent_org_runtime_task_schema_migrations( + name, org_run_id, applied_at + ) VALUES (?1, ?2, ?3)", + params![MIGRATION_SKIPPED_NAME, &run_id, now_rfc3339()], + )?; } } } @@ -159,7 +174,7 @@ pub(super) fn run_is_safe_for_dependency_normalization( ) -> Result { let count: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_org_tasks WHERE org_run_id=?1", + "SELECT COUNT(*) FROM agent_org_runtime_tasks WHERE org_run_id=?1", params![run_id], |row| row.get(0), ) @@ -183,7 +198,7 @@ pub(super) fn run_is_safe_for_dependency_normalization( CASE WHEN metadata_json IS NULL OR length(CAST(metadata_json AS BLOB))<={metadata_max} THEN metadata_json ELSE '!' END AS metadata_json - FROM agent_org_tasks WHERE org_run_id=?1 + FROM agent_org_runtime_tasks WHERE org_run_id=?1 ) AS bounded_tasks" ); let corrupt: i64 = conn diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/read.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/read.rs index a3016d3156..912733203e 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/read.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/read.rs @@ -2,7 +2,7 @@ //! recovery and prompt snapshots, the compact byte-budgeted summary page, the //! open-task-id preview, and (test-only) history listing. -use database::db::get_connection; +use crate::coordination::availability::runtime_connection; use rusqlite::{params, OptionalExtension}; use crate::coordination::agent_org_payload_limits::{ @@ -60,9 +60,9 @@ impl AgentOrgTaskStore { } pub fn get(org_run_id: &str, task_id: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let sql = format!( - "SELECT {SELECT_COLUMNS} FROM agent_org_tasks + "SELECT {SELECT_COLUMNS} FROM agent_org_runtime_tasks WHERE org_run_id=?1 AND id=?2" ); conn.query_row(&sql, params![org_run_id, task_id], row_to_task) @@ -71,7 +71,7 @@ impl AgentOrgTaskStore { } pub fn list(org_run_id: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; list_tasks_with_conn(&conn, org_run_id) } @@ -80,7 +80,7 @@ impl AgentOrgTaskStore { /// behind `get`/`task_get`; a periodic watchdog or model prompt must not /// deserialize up to 64 KiB of result metadata for every task. pub fn list_operational(org_run_id: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::list_operational_with_connection(&conn, org_run_id) } @@ -126,7 +126,7 @@ impl AgentOrgTaskStore { CASE WHEN metadata_json IS NULL OR length(CAST(metadata_json AS BLOB))<=?3 THEN metadata_json ELSE '!' END AS metadata_json - FROM agent_org_tasks + FROM agent_org_runtime_tasks ) task WHERE task.org_run_id=?1 ORDER BY task.created_at ASC, task.id ASC", @@ -183,7 +183,7 @@ impl AgentOrgTaskStore { after_task_id: Option<&str>, limit: usize, ) -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; Self::list_summary_page_with_connection( &conn, org_run_id, @@ -209,7 +209,7 @@ impl AgentOrgTaskStore { let cursor = after_task_id .map(|task_id| { conn.query_row( - "SELECT created_at, id FROM agent_org_tasks + "SELECT created_at, id FROM agent_org_runtime_tasks WHERE org_run_id=?1 AND id=?2 AND length(id)<=?3 AND length(CAST(id AS BLOB))<=?4 AND length(created_at)<=?5 @@ -245,7 +245,7 @@ impl AgentOrgTaskStore { let summary_scalar_predicate = task_summary_scalar_predicate_sql("task"); let filtered_total_sql = format!( - "SELECT COUNT(*) FROM agent_org_tasks task + "SELECT COUNT(*) FROM agent_org_runtime_tasks task WHERE task.org_run_id=?1 AND {summary_scalar_predicate} AND (?2 IS NULL OR task.status=?2) @@ -365,7 +365,7 @@ impl AgentOrgTaskStore { CASE WHEN metadata_json IS NULL OR length(CAST(metadata_json AS BLOB))<=?12 THEN metadata_json ELSE '!' END AS metadata_json - FROM agent_org_tasks + FROM agent_org_runtime_tasks ) task WHERE task.org_run_id=?1 AND {summary_scalar_predicate} @@ -506,7 +506,7 @@ impl AgentOrgTaskStore { let bounded_limit = limit.clamp(1, 500); let mut stmt = conn .prepare( - "SELECT id FROM agent_org_tasks + "SELECT id FROM agent_org_runtime_tasks WHERE org_run_id=?1 AND status<>'completed' AND trim(id)<>'' AND length(id)<=?3 @@ -554,12 +554,12 @@ impl AgentOrgTaskStore { #[cfg(test)] pub fn list_history(org_run_id: &str) -> Result, String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; let mut stmt = conn .prepare( "SELECT id, org_run_id, task_id, event_type, previous_owner, next_owner, previous_status, next_status, actor_member_id, created_at - FROM agent_org_task_events + FROM agent_org_runtime_task_events WHERE org_run_id = ?1 ORDER BY created_at ASC, id ASC", ) diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/requeue.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/requeue.rs index af4a6ece8e..e9063900b8 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/requeue.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/requeue.rs @@ -3,7 +3,9 @@ //! and requeue a failed member's `in_progress` tasks back to the unassigned //! pool. -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use rusqlite::params; use super::super::helpers::{insert_task_history_event, now_rfc3339, row_to_task, SELECT_COLUMNS}; @@ -37,7 +39,7 @@ impl AgentOrgTaskStore { org_run_id: &str, owner_member_id: &str, ) -> Result, String> { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -45,7 +47,7 @@ impl AgentOrgTaskStore { let owned: Vec = { let sql = format!( - "SELECT {SELECT_COLUMNS} FROM agent_org_tasks + "SELECT {SELECT_COLUMNS} FROM agent_org_runtime_tasks WHERE org_run_id = ?1 AND owner = ?2 AND status != ?3 ORDER BY created_at ASC, id ASC" ); @@ -75,7 +77,7 @@ impl AgentOrgTaskStore { .iter() .any(|member_id| member_id != owner_member_id); tx.execute( - "UPDATE agent_org_tasks + "UPDATE agent_org_runtime_tasks SET owner = CASE WHEN ?1 THEN NULL ELSE ?2 END, status = ?3, updated_at = ?4 @@ -154,7 +156,7 @@ impl AgentOrgTaskStore { org_run_id: &str, owner_member_id: &str, ) -> Result, String> { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -162,7 +164,7 @@ impl AgentOrgTaskStore { let owned: Vec = { let sql = format!( - "SELECT {SELECT_COLUMNS} FROM agent_org_tasks + "SELECT {SELECT_COLUMNS} FROM agent_org_runtime_tasks WHERE org_run_id = ?1 AND owner = ?2 AND status = ?3 ORDER BY created_at ASC, id ASC" ); @@ -193,7 +195,7 @@ impl AgentOrgTaskStore { let mut updated_rows = Vec::with_capacity(owned.len()); for task in owned { tx.execute( - "UPDATE agent_org_tasks + "UPDATE agent_org_runtime_tasks SET owner = NULL, status = ?1, updated_at = ?2 WHERE org_run_id = ?3 AND id = ?4 AND owner = ?5 AND status = ?6", params![ @@ -233,17 +235,35 @@ impl AgentOrgTaskStore { mod migration_tests { use super::*; + fn seed_run(conn: &rusqlite::Connection, run_id: &str) { + conn.execute( + "INSERT INTO agent_org_runtime_runs ( + id, org_id, coordinator_agent_id, root_session_id, + org_snapshot_json, entry_mode, status, created_at, updated_at + ) VALUES (?1, 'org-a', 'coordinator-a', 'root-a', '{}', + 'standalone_session', 'idle', + '2026-08-01T00:00:00Z', '2026-08-01T00:00:00Z')", + params![run_id], + ) + .expect("seed runtime run row"); + } + #[test] - fn dependency_migration_skips_corrupt_run_and_normalizes_valid_run() { + fn dependency_migration_terminally_skips_corrupt_run_and_normalizes_valid_run() { let conn = rusqlite::Connection::open_in_memory().expect("open in-memory database"); + crate::coordination::agent_org_runs::init_schema(&conn).expect("create run schema"); super::super::super::init_schema(&conn).expect("create task schema"); + // Discovery is driven from the runs table, so historical task rows + // are only normalized for runs that actually exist. + seed_run(&conn, "valid-run"); + seed_run(&conn, "corrupt-run"); let now = now_rfc3339(); for (id, blocks_json, blocked_by_json) in [("task-a", r#"["task-b"]"#, "[]"), ("task-b", "[]", "[]")] { conn.execute( - "INSERT INTO agent_org_tasks ( + "INSERT INTO agent_org_runtime_tasks ( id, org_run_id, subject, description, status, blocks_json, blocked_by_json, created_at, updated_at ) VALUES (?1, 'valid-run', ?1, '', 'pending', ?2, ?3, ?4, ?4)", @@ -252,7 +272,7 @@ mod migration_tests { .expect("seed valid legacy task"); } conn.execute( - "INSERT INTO agent_org_tasks ( + "INSERT INTO agent_org_runtime_tasks ( id, org_run_id, subject, description, status, blocks_json, blocked_by_json, created_at, updated_at ) VALUES ( @@ -269,8 +289,8 @@ mod migration_tests { let (a_blocks, b_blocked_by): (String, String) = conn .query_row( "SELECT a.blocks_json, b.blocked_by_json - FROM agent_org_tasks a - JOIN agent_org_tasks b + FROM agent_org_runtime_tasks a + JOIN agent_org_runtime_tasks b ON b.org_run_id=a.org_run_id AND b.id='task-b' WHERE a.org_run_id='valid-run' AND a.id='task-a'", [], @@ -282,7 +302,7 @@ mod migration_tests { let corrupt_blocks: String = conn .query_row( - "SELECT blocks_json FROM agent_org_tasks + "SELECT blocks_json FROM agent_org_runtime_tasks WHERE org_run_id='corrupt-run' AND id='corrupt-task'", [], |row| row.get(0), @@ -290,52 +310,85 @@ mod migration_tests { .expect("corrupt row remains available for runtime repair"); assert_eq!(corrupt_blocks, "not-json"); - let valid_marked: bool = conn - .query_row( - "SELECT EXISTS( - SELECT 1 FROM agent_org_task_run_schema_migrations - WHERE name='canonical_blocked_by_v1' AND org_run_id='valid-run' - )", - [], - |row| row.get(0), - ) - .expect("read valid marker"); - let corrupt_marked: bool = conn - .query_row( + let marker = |name: &str, run: &str| -> bool { + conn.query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_task_run_schema_migrations - WHERE name='canonical_blocked_by_v1' AND org_run_id='corrupt-run' + SELECT 1 FROM agent_org_runtime_task_schema_migrations + WHERE name=?1 AND org_run_id=?2 )", - [], + params![name, run], |row| row.get(0), ) - .expect("read corrupt marker"); - assert!(valid_marked, "healthy run receives its own success marker"); + .expect("read migration marker") + }; + assert!( + marker("canonical_blocked_by_v1", "valid-run"), + "healthy run receives its own success marker" + ); + assert!( + !marker("canonical_blocked_by_v1", "corrupt-run"), + "corrupt run is never marked as normalized" + ); assert!( - !corrupt_marked, - "corrupt run remains unmarked so a later startup can retry" + marker("canonical_blocked_by_v1_skipped", "corrupt-run"), + "corrupt run is terminally skipped after one warn instead of \ + being rediscovered and re-warned on every boot" ); + // Even after the row is repaired, the terminal skip holds: the + // boot migration never revisits the run (repair goes through the + // runtime repair surfaces, which normalize on write). conn.execute( - "UPDATE agent_org_tasks SET blocks_json='[]' + "UPDATE agent_org_runtime_tasks SET blocks_json='[]' WHERE org_run_id='corrupt-run' AND id='corrupt-task'", [], ) .expect("repair corrupt historical row"); - super::super::super::init_schema(&conn).expect("retry repaired run"); - let corrupt_marked_after_retry: bool = conn + super::super::super::init_schema(&conn).expect("boot after repair"); + assert!( + !marker("canonical_blocked_by_v1", "corrupt-run"), + "terminally skipped run is not retried by the boot migration" + ); + assert!(marker("canonical_blocked_by_v1_skipped", "corrupt-run")); + } + + #[test] + fn discovery_never_rescans_marked_runs() { + let conn = rusqlite::Connection::open_in_memory().expect("open in-memory database"); + crate::coordination::agent_org_runs::init_schema(&conn).expect("create run schema"); + super::super::super::init_schema(&conn).expect("create task schema"); + seed_run(&conn, "settled-run"); + + // First boot discovers the run (no tasks — trivially normalized). + super::super::super::init_schema(&conn).expect("marker boot"); + let applied_at: String = conn .query_row( - "SELECT EXISTS( - SELECT 1 FROM agent_org_task_run_schema_migrations - WHERE name='canonical_blocked_by_v1' AND org_run_id='corrupt-run' - )", + "SELECT applied_at FROM agent_org_runtime_task_schema_migrations + WHERE name='canonical_blocked_by_v1' AND org_run_id='settled-run'", [], |row| row.get(0), ) - .expect("read retry marker"); - assert!( - corrupt_marked_after_retry, - "a repaired run is retried and marked independently" - ); + .expect("run marked on first boot"); + + // Subsequent boots leave the marker untouched (anti-join excludes it). + super::super::super::init_schema(&conn).expect("steady-state boot"); + let applied_at_after: String = conn + .query_row( + "SELECT applied_at FROM agent_org_runtime_task_schema_migrations + WHERE name='canonical_blocked_by_v1' AND org_run_id='settled-run'", + [], + |row| row.get(0), + ) + .expect("marker survives"); + assert_eq!(applied_at, applied_at_after); + let marker_rows: i64 = conn + .query_row( + "SELECT COUNT(*) FROM agent_org_runtime_task_schema_migrations + WHERE org_run_id='settled-run'", + [], + |row| row.get(0), + ) + .expect("count markers"); + assert_eq!(marker_rows, 1); } } diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/update.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/update.rs index 71b4b918c7..366332e2f5 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/update.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/update.rs @@ -3,7 +3,9 @@ //! `if_unchanged` variants), and the shared `update_inner` that recanonicalizes //! dependencies and reports a `TaskMutationOutcome`. -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use rusqlite::{params, OptionalExtension}; use crate::coordination::agent_org_payload_limits::validate_task_dependency_ids; @@ -46,7 +48,7 @@ impl AgentOrgTaskStore { ) -> Result { ensure_run_allows_task_mutation(tx, org_run_id)?; let sql = format!( - "SELECT {SELECT_COLUMNS} FROM agent_org_tasks + "SELECT {SELECT_COLUMNS} FROM agent_org_runtime_tasks WHERE org_run_id = ?1 AND id = ?2" ); let previous: Option = tx @@ -94,7 +96,7 @@ impl AgentOrgTaskStore { let metadata_json = encode_metadata(current.metadata.as_ref())?; let changed = tx .execute( - "UPDATE agent_org_tasks + "UPDATE agent_org_runtime_tasks SET status = ?1, metadata_json = ?2, updated_at = ?3 WHERE org_run_id = ?4 AND id = ?5 AND status = ?6 AND owner = ?7", params![ @@ -217,7 +219,7 @@ impl AgentOrgTaskStore { if let Some(blocked_by) = patch.blocked_by.as_ref() { validate_task_dependency_ids("blocked_by", blocked_by)?; } - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -329,7 +331,7 @@ impl AgentOrgTaskStore { let metadata_json = encode_metadata(task.metadata.as_ref())?; tx.execute( - "UPDATE agent_org_tasks SET + "UPDATE agent_org_runtime_tasks SET subject = ?1, description = ?2, active_form = ?3, diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/validation.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/validation.rs index 2c46e1d8c1..d36301425c 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/validation.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/store/validation.rs @@ -66,7 +66,7 @@ pub(super) fn ensure_run_allows_task_mutation( ) -> Result<(), String> { let status: Option = conn .query_row( - "SELECT status FROM agent_org_runs WHERE id=?1", + "SELECT status FROM agent_org_runtime_runs WHERE id=?1", params![org_run_id], |row| row.get(0), ) @@ -232,7 +232,7 @@ pub(super) fn validate_task_persistence_invariants( let snapshot_json: Option = conn .query_row( - "SELECT org_snapshot_json FROM agent_org_runs WHERE id=?1", + "SELECT org_snapshot_json FROM agent_org_runtime_runs WHERE id=?1", params![org_run_id], |row| row.get(0), ) diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/tests.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/tests.rs index ca80eae6fa..50baa7c83f 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/tests.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_tasks/tests.rs @@ -6,7 +6,7 @@ fn make_params(org_run_id: &str, id: &str, subject: &str) -> CreateTaskParams { let conn = get_connection().expect("task test database"); let now = chrono::Utc::now().to_rfc3339(); conn.execute( - "INSERT OR IGNORE INTO agent_org_runs + "INSERT OR IGNORE INTO agent_org_runtime_runs (id, org_id, coordinator_agent_id, entry_mode, status, created_at, updated_at) VALUES (?1, 'task-test-org', 'task-test-coordinator', 'standalone_session', 'running', ?2, ?2)", rusqlite::params![org_run_id, now], @@ -124,7 +124,7 @@ fn task_mutations_require_running_parent_run() { crate::coordination::agent_org_runs::init_schema(&conn).expect("run schema"); let now = chrono::Utc::now().to_rfc3339(); conn.execute( - "INSERT INTO agent_org_runs + "INSERT INTO agent_org_runtime_runs (id, org_id, coordinator_agent_id, entry_mode, status, created_at, updated_at) VALUES ('guarded-run', 'org', 'coord', 'standalone_session', 'paused', ?1, ?1)", rusqlite::params![now], @@ -141,7 +141,7 @@ fn task_mutations_require_running_parent_run() { assert!(create_error.contains("agent_org_run_not_mutable")); conn.execute( - "UPDATE agent_org_runs SET status='running' WHERE id='guarded-run'", + "UPDATE agent_org_runtime_runs SET status='running' WHERE id='guarded-run'", [], ) .unwrap(); @@ -153,7 +153,7 @@ fn task_mutations_require_running_parent_run() { )) .expect("running run permits create"); conn.execute( - "UPDATE agent_org_runs SET status='archived' WHERE id='guarded-run'", + "UPDATE agent_org_runtime_runs SET status='archived' WHERE id='guarded-run'", [], ) .unwrap(); @@ -387,7 +387,7 @@ fn delete_rejects_task_used_as_an_inbox_delivery_replacement() { let conn = get_connection().expect("test sqlite connection"); let now = chrono::Utc::now().to_rfc3339(); conn.execute( - "INSERT INTO agent_inbox ( + "INSERT INTO agent_org_runtime_inbox ( recipient_agent_id, sender_agent_id, org_run_id, payload_kind, payload_json, created_at ) VALUES ( @@ -399,7 +399,7 @@ fn delete_rejects_task_used_as_an_inbox_delivery_replacement() { .expect("seed source inbox evidence"); let inbox_id = conn.last_insert_rowid(); conn.execute( - "INSERT INTO agent_inbox_delivery_resolutions ( + "INSERT INTO agent_org_runtime_inbox_delivery_resolutions ( inbox_id, org_run_id, resolution_kind, resolved_by_member_id, reason, replacement_task_id, created_at ) VALUES (?1, ?2, 'superseded', 'coordinator', 'Moved to task', @@ -427,12 +427,15 @@ fn delete_fails_closed_when_delivery_resolution_schema_is_missing() { )) .expect("create guarded task"); let conn = get_connection().expect("test sqlite connection"); - conn.execute("DROP TABLE agent_inbox_delivery_resolutions", []) - .expect("simulate damaged delivery-resolution schema"); + conn.execute( + "DROP TABLE agent_org_runtime_inbox_delivery_resolutions", + [], + ) + .expect("simulate damaged delivery-resolution schema"); let error = AgentOrgTaskStore::delete(&run_id, "schema-guarded-task") .expect_err("schema failure must not be treated as an unreferenced task"); - assert!(error.contains("agent_inbox_delivery_resolutions")); + assert!(error.contains("agent_org_runtime_inbox_delivery_resolutions")); assert!(AgentOrgTaskStore::get(&run_id, "schema-guarded-task") .expect("reload guarded task") .is_some()); @@ -512,7 +515,7 @@ fn corrupt_predicate_flags_ownerless_in_progress_and_spaced_eligibility() { let conn = get_connection().expect("task database"); let now = chrono::Utc::now().to_rfc3339(); conn.execute( - "INSERT INTO agent_org_tasks + "INSERT INTO agent_org_runtime_tasks (id, org_run_id, subject, description, active_form, owner, status, blocks_json, blocked_by_json, metadata_json, created_at, updated_at) VALUES ('ownerless-running', ?1, 'bad running row', '', NULL, NULL, @@ -525,7 +528,7 @@ fn corrupt_predicate_flags_ownerless_in_progress_and_spaced_eligibility() { ) .expect("seed ownerless in-progress row"); conn.execute( - "INSERT INTO agent_org_tasks + "INSERT INTO agent_org_runtime_tasks (id, org_run_id, subject, description, active_form, owner, status, blocks_json, blocked_by_json, metadata_json, created_at, updated_at) VALUES ('spaced-eligibility', ?1, 'bad eligibility row', '', NULL, NULL, @@ -542,7 +545,9 @@ fn corrupt_predicate_flags_ownerless_in_progress_and_spaced_eligibility() { let predicate = corrupt_task_row_predicate_sql(); let corrupt_count: i64 = conn .query_row( - &format!("SELECT COUNT(*) FROM agent_org_tasks WHERE org_run_id=?1 AND {predicate}"), + &format!( + "SELECT COUNT(*) FROM agent_org_runtime_tasks WHERE org_run_id=?1 AND {predicate}" + ), rusqlite::params![&run_id], |row| row.get(0), ) @@ -561,7 +566,7 @@ fn summary_filtered_total_matches_rows_after_scalar_corruption_filtering() { let oversized_id = "x".repeat(crate::coordination::agent_org_payload_limits::TASK_IDENTIFIER_MAX_CHARS + 1); conn.execute( - "INSERT INTO agent_org_tasks + "INSERT INTO agent_org_runtime_tasks (id, org_run_id, subject, description, active_form, owner, status, blocks_json, blocked_by_json, metadata_json, created_at, updated_at) VALUES (?1, ?2, 'hidden corrupt row', '', NULL, NULL, 'pending', @@ -673,7 +678,7 @@ fn store_rejects_malformed_reserved_dispatch_metadata() { let conn = get_connection().expect("task database"); let now = chrono::Utc::now().to_rfc3339(); conn.execute( - "INSERT INTO agent_org_tasks + "INSERT INTO agent_org_runtime_tasks (id, org_run_id, subject, description, active_form, owner, status, blocks_json, blocked_by_json, metadata_json, created_at, updated_at) VALUES ('historical-output-producer', ?1, 'historical', '', NULL, @@ -685,7 +690,7 @@ fn store_rejects_malformed_reserved_dispatch_metadata() { let classified: bool = conn .query_row( &format!( - "SELECT {predicate} FROM agent_org_tasks + "SELECT {predicate} FROM agent_org_runtime_tasks WHERE org_run_id=?1 AND id='historical-output-producer'" ), rusqlite::params![&run_id], @@ -708,7 +713,7 @@ fn store_rejects_malformed_reserved_dispatch_metadata() { }, }); conn.execute( - "INSERT INTO agent_org_tasks + "INSERT INTO agent_org_runtime_tasks (id, org_run_id, subject, description, active_form, owner, status, blocks_json, blocked_by_json, metadata_json, created_at, updated_at) VALUES ('historical-output-zone', ?1, 'historical', '', NULL, @@ -719,7 +724,7 @@ fn store_rejects_malformed_reserved_dispatch_metadata() { let classified: bool = conn .query_row( &format!( - "SELECT {predicate} FROM agent_org_tasks + "SELECT {predicate} FROM agent_org_runtime_tasks WHERE org_run_id=?1 AND id='historical-output-zone'" ), rusqlite::params![&run_id], diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog.rs index e3dd48e79b..2b6d6c7fef 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog.rs @@ -35,6 +35,7 @@ mod reservation; #[cfg(test)] mod tests; +pub(crate) use budget::create_schema; pub use budget::init_schema; pub use budget::{ clear_rewake_budget, startup_prune_recovery_state, AgentOrgRecoveryStartupPruneReport, @@ -56,7 +57,9 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use std::time::{Duration, Instant}; use chrono::{DateTime, Duration as ChronoDuration, Utc}; -use database::db::{get_connection, with_sessions_writer}; +use database::db::with_sessions_writer; + +use crate::coordination::availability::runtime_connection; use rusqlite::{params, Connection, OptionalExtension}; use tauri::AppHandle; diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/budget.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/budget.rs index 68b9ef312f..526e08f283 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/budget.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/budget.rs @@ -6,9 +6,16 @@ use super::*; +/// Tests-only convenience: production initialization goes through the +/// namespace coordinator (`coordination::schema::initialize`), never this +/// module-level entry point. pub fn init_schema(conn: &Connection) -> rusqlite::Result<()> { + create_schema(conn) +} + +pub(crate) fn create_schema(conn: &Connection) -> rusqlite::Result<()> { conn.execute_batch( - "CREATE TABLE IF NOT EXISTS agent_org_recovery_attempts ( + "CREATE TABLE IF NOT EXISTS agent_org_runtime_recovery_attempts ( org_run_id TEXT NOT NULL, action_kind TEXT NOT NULL, target_key TEXT NOT NULL, @@ -19,8 +26,8 @@ pub fn init_schema(conn: &Connection) -> rusqlite::Result<()> { reservation_token TEXT, PRIMARY KEY (org_run_id, action_kind, target_key) ); - CREATE INDEX IF NOT EXISTS idx_agent_org_recovery_attempts_run - ON agent_org_recovery_attempts(org_run_id);", + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_recovery_attempts_run + ON agent_org_runtime_recovery_attempts(org_run_id);", ) } @@ -38,7 +45,7 @@ pub(super) fn budget_disposition( target_key: &str, fingerprint: &str, ) -> Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; budget_disposition_with_connection(&conn, run_id, action_kind, target_key, fingerprint) } @@ -52,7 +59,7 @@ pub(super) fn budget_disposition_with_connection( let row: Option<(String, i64, String)> = conn .query_row( "SELECT reason_fingerprint, attempts, next_allowed_at - FROM agent_org_recovery_attempts + FROM agent_org_runtime_recovery_attempts WHERE org_run_id=?1 AND action_kind=?2 AND target_key=?3", params![run_id, action_kind, target_key], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), @@ -104,7 +111,7 @@ pub(super) fn record_attempt( fingerprint: &str, ) -> Result<(), String> { with_sessions_writer(|| -> Result<(), String> { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -125,7 +132,7 @@ pub(super) fn record_attempt_with_connection( ) -> Result<(), String> { let previous: Option<(String, i64)> = conn .query_row( - "SELECT reason_fingerprint, attempts FROM agent_org_recovery_attempts + "SELECT reason_fingerprint, attempts FROM agent_org_runtime_recovery_attempts WHERE org_run_id=?1 AND action_kind=?2 AND target_key=?3", params![run_id, action_kind, target_key], |row| Ok((row.get(0)?, row.get(1)?)), @@ -143,7 +150,7 @@ pub(super) fn record_attempt_with_connection( let now = Utc::now(); let next = now + ChronoDuration::seconds(RECOVERY_DELAYS_SECS[delay_index]); conn.execute( - "INSERT INTO agent_org_recovery_attempts + "INSERT INTO agent_org_runtime_recovery_attempts (org_run_id, action_kind, target_key, reason_fingerprint, attempts, next_allowed_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(org_run_id, action_kind, target_key) DO UPDATE SET @@ -194,9 +201,9 @@ pub fn startup_prune_recovery_state() -> Result Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; conn.execute( - "UPDATE agent_org_recovery_attempts + "UPDATE agent_org_runtime_recovery_attempts SET reservation_token=NULL WHERE reservation_token IS NOT NULL", [], @@ -206,14 +213,14 @@ pub fn startup_prune_recovery_state() -> Result Result { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; conn.execute( - "DELETE FROM agent_org_recovery_attempts + "DELETE FROM agent_org_runtime_recovery_attempts WHERE rowid IN ( SELECT attempt.rowid - FROM agent_org_recovery_attempts attempt + FROM agent_org_runtime_recovery_attempts attempt WHERE NOT EXISTS ( - SELECT 1 FROM agent_org_runs run + SELECT 1 FROM agent_org_runtime_runs run WHERE run.id=attempt.org_run_id AND run.status IN ('starting', 'running') ) @@ -233,9 +240,9 @@ pub fn startup_prune_recovery_state() -> Result Result<(), String> { with_sessions_writer(|| { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; conn.execute( - "DELETE FROM agent_org_recovery_attempts + "DELETE FROM agent_org_runtime_recovery_attempts WHERE org_run_id=?1 AND action_kind=?2 AND target_key=?3", params![run_id, MEMBER_REWAKE, member_id], ) diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/inspect.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/inspect.rs index 164705d697..c03f3ccaeb 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/inspect.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/inspect.rs @@ -81,7 +81,7 @@ fn corrupt_task_repair_facts( ) -> Result, String> { let task_count: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_org_tasks WHERE org_run_id=?1", + "SELECT COUNT(*) FROM agent_org_runtime_tasks WHERE org_run_id=?1", params![run_id], |row| row.get(0), ) @@ -113,7 +113,7 @@ fn corrupt_task_repair_facts( CASE WHEN metadata_json IS NULL OR length(CAST(metadata_json AS BLOB))<={metadata_max} THEN metadata_json ELSE '!' END AS metadata_json - FROM agent_org_tasks WHERE org_run_id=?1 + FROM agent_org_runtime_tasks WHERE org_run_id=?1 ) AS bounded_tasks WHERE {predicate} ORDER BY id ASC" @@ -718,7 +718,7 @@ fn coordinator_notice_budget_exists_with_connection( ) -> Result { conn.query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_recovery_attempts + SELECT 1 FROM agent_org_runtime_recovery_attempts WHERE org_run_id=?1 AND action_kind=?2 AND target_key='coordinator' )", params![run_id, COORDINATOR_NOTICE], @@ -808,7 +808,7 @@ pub(super) fn stale_in_flight_intent_repairs_with_connection( } pub fn inspect_stalled_run(run_id: &str) -> Result { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Deferred) .map_err(|err| err.to_string())?; diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/recover.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/recover.rs index 723aabedee..e810a6e796 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/recover.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/recover.rs @@ -305,14 +305,14 @@ pub(super) fn repair_stale_in_flight_intents( repairs: &[StaleTurnIntentRepair], ) -> Result { with_sessions_writer(|| -> Result { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; let running: bool = tx .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_runs WHERE id=?1 AND status='running' + SELECT 1 FROM agent_org_runtime_runs WHERE id=?1 AND status='running' )", params![run_id], |row| row.get(0), @@ -365,13 +365,13 @@ pub(super) fn repair_stale_in_flight_intents( fn clear_coordinator_notice_budget_if_recovered(run_id: &str) -> Result<(), String> { with_sessions_writer(|| -> Result<(), String> { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; if !inspect_stalled_run_with_connection(&tx, run_id)?.coordinator_repair_active { tx.execute( - "DELETE FROM agent_org_recovery_attempts + "DELETE FROM agent_org_runtime_recovery_attempts WHERE org_run_id=?1 AND action_kind=?2 AND target_key='coordinator'", params![run_id, COORDINATOR_NOTICE], ) @@ -404,14 +404,14 @@ fn insert_member_continuation_if_tasks_current( action: &MemberContinuationAction, ) -> Result { with_sessions_writer(|| -> Result { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; let running: bool = tx .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_runs WHERE id=?1 AND status='running' + SELECT 1 FROM agent_org_runtime_runs WHERE id=?1 AND status='running' )", params![run_id], |row| row.get(0), @@ -436,11 +436,11 @@ fn insert_member_continuation_if_tasks_current( let has_unread: bool = tx .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_inbox + SELECT 1 FROM agent_org_runtime_inbox WHERE org_run_id=?1 AND recipient_member_id=?2 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id ) )", params![run_id, &action.member_id], @@ -459,7 +459,7 @@ fn insert_member_continuation_if_tasks_current( let pending_plan_task_ids = { let mut stmt = tx .prepare( - "SELECT source_task_id FROM agent_org_plan_approvals + "SELECT source_task_id FROM agent_org_runtime_plan_approvals WHERE org_run_id=?1 AND status='pending'", ) .map_err(|err| err.to_string())?; @@ -533,7 +533,7 @@ fn insert_coordinator_stall_notice( expected_inbox_fingerprint: Option<&str>, ) -> Result { with_sessions_writer(|| -> Result { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -545,7 +545,7 @@ fn insert_coordinator_stall_notice( let coordinator_runtime: Option<(String, Option, Option)> = tx .query_row( "SELECT run.coordinator_agent_id, session.status, session.updated_at - FROM agent_org_runs run + FROM agent_org_runtime_runs run LEFT JOIN agent_sessions session ON session.session_id=run.root_session_id WHERE run.id=?1 AND run.status='running'", @@ -563,7 +563,7 @@ fn insert_coordinator_stall_notice( let current_work_revision = tx .query_row( - "SELECT work_revision FROM agent_org_run_progress WHERE org_run_id=?1", + "SELECT work_revision FROM agent_org_runtime_run_progress WHERE org_run_id=?1", params![run_id], |row| row.get::<_, i64>(0), ) @@ -704,13 +704,13 @@ fn insert_coordinator_stall_notice( let coordinator_has_unread: bool = tx .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_inbox + SELECT 1 FROM agent_org_runtime_inbox WHERE org_run_id=?1 AND recipient_member_id=?2 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id ) )", params![run_id, COORDINATOR_MEMBER_ID], diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/reservation.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/reservation.rs index 740af8c244..b7c4c02103 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/reservation.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/reservation.rs @@ -35,7 +35,7 @@ pub(crate) fn reserve_member_rewake_dispatch( fingerprint: &str, ) -> Result { with_sessions_writer(|| -> Result { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; @@ -51,7 +51,7 @@ pub(crate) fn reserve_member_rewake_dispatch( .query_row( "SELECT reason_fingerprint, attempts, next_allowed_at, updated_at, reservation_token - FROM agent_org_recovery_attempts + FROM agent_org_runtime_recovery_attempts WHERE org_run_id=?1 AND action_kind=?2 AND target_key=?3", params![run_id, MEMBER_REWAKE, member_id], |row| { @@ -70,7 +70,7 @@ pub(crate) fn reserve_member_rewake_dispatch( let token = uuid::Uuid::new_v4().to_string(); let updated = tx .execute( - "UPDATE agent_org_recovery_attempts + "UPDATE agent_org_runtime_recovery_attempts SET reservation_token=?1 WHERE org_run_id=?2 AND action_kind=?3 AND target_key=?4 AND reason_fingerprint=?5", @@ -96,9 +96,9 @@ pub(crate) fn commit_member_rewake_reservation( reservation: &MemberRewakeReservation, ) -> Result<(), String> { with_sessions_writer(|| -> Result<(), String> { - let conn = get_connection().map_err(|err| err.to_string())?; + let conn = runtime_connection().map_err(|err| err.to_string())?; conn.execute( - "UPDATE agent_org_recovery_attempts + "UPDATE agent_org_runtime_recovery_attempts SET reservation_token=NULL WHERE org_run_id=?1 AND action_kind=?2 AND target_key=?3 AND reservation_token=?4", @@ -118,14 +118,14 @@ pub(crate) fn refund_member_rewake_reservation( reservation: &MemberRewakeReservation, ) -> Result { with_sessions_writer(|| -> Result { - let mut conn = get_connection().map_err(|err| err.to_string())?; + let mut conn = runtime_connection().map_err(|err| err.to_string())?; let tx = conn .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) .map_err(|err| err.to_string())?; let owns_current: bool = tx .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_recovery_attempts + SELECT 1 FROM agent_org_runtime_recovery_attempts WHERE org_run_id=?1 AND action_kind=?2 AND target_key=?3 AND reservation_token=?4 )", @@ -145,7 +145,7 @@ pub(crate) fn refund_member_rewake_reservation( if let Some(previous) = reservation.previous.as_ref() { tx.execute( - "UPDATE agent_org_recovery_attempts + "UPDATE agent_org_runtime_recovery_attempts SET reason_fingerprint=?1, attempts=?2, next_allowed_at=?3, updated_at=?4, reservation_token=?5 WHERE org_run_id=?6 AND action_kind=?7 AND target_key=?8 @@ -165,7 +165,7 @@ pub(crate) fn refund_member_rewake_reservation( .map_err(|err| err.to_string())?; } else { tx.execute( - "DELETE FROM agent_org_recovery_attempts + "DELETE FROM agent_org_runtime_recovery_attempts WHERE org_run_id=?1 AND action_kind=?2 AND target_key=?3 AND reservation_token=?4", params![ diff --git a/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/tests.rs b/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/tests.rs index 13b8290345..f3c527a738 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/tests.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/agent_org_watchdog/tests.rs @@ -9,7 +9,7 @@ use super::*; use crate::coordination::agent_org_runs::{ AgentOrgRunEntryMode, AgentOrgRunRecord, CreateAgentOrgRunParams, }; -use crate::definitions::orgs::OrgDefinition; +use crate::definitions::orgs::{FlatOrgMember, OrgDefinition}; fn fake_run(id: &str) -> AgentOrgRunRecord { let now = Utc::now().to_rfc3339(); @@ -46,7 +46,7 @@ fn wakeable_status_includes_idle_and_terminal_but_not_running() { #[test] fn member_rewake_reservation_is_atomic_and_refundable() { let _sandbox = test_helpers::test_env::sandbox(); - let conn = get_connection().expect("db"); + let conn = runtime_connection().expect("db"); init_schema(&conn).expect("schema"); let run_id = format!("run-{}", uuid::Uuid::new_v4()); let member_id = "member-reserved"; @@ -74,7 +74,7 @@ fn member_rewake_reservation_is_atomic_and_refundable() { #[test] fn stale_rewake_refund_cannot_undo_newer_input() { let _sandbox = test_helpers::test_env::sandbox(); - let conn = get_connection().expect("db"); + let conn = runtime_connection().expect("db"); init_schema(&conn).expect("schema"); let run_id = format!("run-{}", uuid::Uuid::new_v4()); let member_id = "member-new-input"; @@ -152,12 +152,12 @@ fn shared_scan_deadline_is_checked_at_each_team_boundary() { #[test] fn running_query_is_limited_and_never_visits_quiet_states() { let _sandbox = test_helpers::test_env::sandbox(); - let conn = get_connection().expect("db"); + let conn = runtime_connection().expect("db"); crate::coordination::init_agent_org_schemas(&conn).expect("Agent Org schemas"); let now = Utc::now().to_rfc3339(); for index in 0..105 { conn.execute( - "INSERT INTO agent_org_runs ( + "INSERT INTO agent_org_runtime_runs ( id, org_id, coordinator_agent_id, root_session_id, entry_mode, status, created_at, updated_at ) VALUES (?1, 'watchdog-org', 'coordinator', ?2, 'standalone_session', @@ -172,7 +172,7 @@ fn running_query_is_limited_and_never_visits_quiet_states() { } for status in ["starting", "paused", "idle", "failed", "archived"] { conn.execute( - "INSERT INTO agent_org_runs ( + "INSERT INTO agent_org_runtime_runs ( id, org_id, coordinator_agent_id, root_session_id, entry_mode, status, created_at, updated_at ) VALUES (?1, 'watchdog-org', 'coordinator', ?2, 'standalone_session', @@ -199,12 +199,12 @@ fn running_query_is_limited_and_never_visits_quiet_states() { #[test] fn startup_prune_clears_all_reservations_and_non_running_budget_rows() { let _sandbox = test_helpers::test_env::sandbox(); - let conn = get_connection().expect("db"); + let conn = runtime_connection().expect("db"); crate::coordination::init_agent_org_schemas(&conn).expect("Agent Org schemas"); let now = Utc::now().to_rfc3339(); for (run_id, status) in [("prune-running", "running"), ("prune-idle", "idle")] { conn.execute( - "INSERT INTO agent_org_runs ( + "INSERT INTO agent_org_runtime_runs ( id, org_id, coordinator_agent_id, root_session_id, entry_mode, status, created_at, updated_at ) VALUES (?1, 'prune-org', 'coordinator', ?2, 'standalone_session', ?3, ?4, ?4)", @@ -218,7 +218,7 @@ fn startup_prune_clears_all_reservations_and_non_running_budget_rows() { ("prune-missing-run", None::<&str>), ] { conn.execute( - "INSERT INTO agent_org_recovery_attempts + "INSERT INTO agent_org_runtime_recovery_attempts (org_run_id, action_kind, target_key, reason_fingerprint, attempts, next_allowed_at, updated_at, reservation_token) VALUES (?1, ?2, 'member-x', 'fp', 1, ?3, ?3, ?4)", @@ -233,7 +233,7 @@ fn startup_prune_clears_all_reservations_and_non_running_budget_rows() { let leaked_tokens: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_org_recovery_attempts + "SELECT COUNT(*) FROM agent_org_runtime_recovery_attempts WHERE reservation_token IS NOT NULL", [], |row| row.get(0), @@ -242,7 +242,7 @@ fn startup_prune_clears_all_reservations_and_non_running_budget_rows() { assert_eq!(leaked_tokens, 0, "no reservation survives its process"); let remaining: Vec = { let mut stmt = conn - .prepare("SELECT org_run_id FROM agent_org_recovery_attempts ORDER BY org_run_id") + .prepare("SELECT org_run_id FROM agent_org_runtime_recovery_attempts ORDER BY org_run_id") .expect("prepare"); let rows = stmt .query_map([], |row| row.get::<_, String>(0)) @@ -259,13 +259,13 @@ fn startup_prune_clears_all_reservations_and_non_running_budget_rows() { #[test] fn rotation_cursor_visits_every_working_run_across_ticks() { let _sandbox = test_helpers::test_env::sandbox(); - let conn = get_connection().expect("db"); + let conn = runtime_connection().expect("db"); crate::coordination::init_agent_org_schemas(&conn).expect("Agent Org schemas"); const POPULATION: usize = 5; const TICK_LIMIT: usize = 2; for index in 0..POPULATION { conn.execute( - "INSERT INTO agent_org_runs ( + "INSERT INTO agent_org_runtime_runs ( id, org_id, coordinator_agent_id, root_session_id, entry_mode, status, created_at, updated_at ) VALUES (?1, 'rotation-org', 'coordinator', ?2, 'standalone_session', @@ -311,7 +311,7 @@ fn rotation_cursor_visits_every_working_run_across_ticks() { #[test] fn coordinator_notice_budget_backs_off_and_resets_on_new_reason() { let _sandbox = test_helpers::test_env::sandbox(); - let conn = get_connection().expect("db"); + let conn = runtime_connection().expect("db"); init_schema(&conn).expect("schema"); let run_id = format!("run-{}", uuid::Uuid::new_v4()); @@ -321,7 +321,7 @@ fn coordinator_notice_budget_backs_off_and_resets_on_new_reason() { } fn ensure_watchdog_runtime_schemas() { - let conn = get_connection().expect("test sqlite connection"); + let conn = runtime_connection().expect("test sqlite connection"); crate::foundation::persistence::test_schema::ensure_agent_sessions_schema(&conn); crate::foundation::persistence::session_snapshots::ensure_tables_with(&conn) .expect("agent sessions schema"); @@ -377,16 +377,24 @@ fn seed_wedged_working_run(intent_updated_at: &str) -> String { org_id: "wedged-org".to_string(), coordinator_agent_id: "agent-coord".to_string(), root_session_id: Some(root_session_id.clone()), - org_snapshot: OrgDefinition { + org_snapshot: (&OrgDefinition { id: "wedged-org".to_string(), name: "Wedged Org".to_string(), role: "lead".to_string(), agent_id: "agent-coord".to_string(), description: None, - hierarchy_mode: Default::default(), plan_approval_policy: Default::default(), - children: Vec::new(), - }, + members: vec![FlatOrgMember { + member_id: "wedged-member".to_string(), + name: "Wedged Member".to_string(), + role: "worker".to_string(), + agent_id: "agent-coord".to_string(), + runtime_config: None, + }], + additional_task_graph_writer_member_ids: Vec::new(), + member_communication_links: Vec::new(), + }) + .into(), entry_mode: AgentOrgRunEntryMode::StandaloneSession, status: AgentOrgRunStatus::Running, work_item_id: None, @@ -413,7 +421,7 @@ fn seed_wedged_working_run(intent_updated_at: &str) -> String { AgentOrgRunStore::mark_coordinator_observed_work_revision(&run.id, revision) .expect("mark coordinator observed revision"); - let conn = get_connection().expect("test sqlite connection"); + let conn = runtime_connection().expect("test sqlite connection"); conn.execute( "INSERT INTO session_turn_intents ( session_id, turn_intent_id, org_run_id, source, status, @@ -449,7 +457,7 @@ fn wedged_running_intent_older_than_grace_is_repaired_and_run_can_idle() { let repaired = repair_stale_in_flight_intents(&run_id, &plan.stale_intent_repairs).expect("repair"); assert_eq!(repaired, 1); - let status: String = get_connection() + let status: String = runtime_connection() .expect("db") .query_row( "SELECT status FROM session_turn_intents WHERE turn_intent_id='wedged-turn-intent'", @@ -496,7 +504,7 @@ fn young_running_intent_is_never_auto_repaired() { #[test] fn rewake_budget_exhaustion_requires_all_attempts_and_an_expired_cooldown() { let _sandbox = test_helpers::test_env::sandbox(); - let conn = get_connection().expect("db"); + let conn = runtime_connection().expect("db"); init_schema(&conn).expect("schema"); let run_id = format!("run-{}", uuid::Uuid::new_v4()); let member_id = "member-exhausted"; @@ -504,7 +512,7 @@ fn rewake_budget_exhaustion_requires_all_attempts_and_an_expired_cooldown() { assert!(!rewake_budget_exhausted(&run_id, member_id, fingerprint).expect("initial budget")); let expired_at = (Utc::now() - ChronoDuration::seconds(1)).to_rfc3339(); conn.execute( - "INSERT INTO agent_org_recovery_attempts + "INSERT INTO agent_org_runtime_recovery_attempts (org_run_id, action_kind, target_key, reason_fingerprint, attempts, next_allowed_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6)", diff --git a/src-tauri/crates/agent-core/src/core/coordination/availability.rs b/src-tauri/crates/agent-core/src/core/coordination/availability.rs new file mode 100644 index 0000000000..7c8ae43ace --- /dev/null +++ b/src-tauri/crates/agent-core/src/core/coordination/availability.rs @@ -0,0 +1,111 @@ +//! Process-global Agent Org runtime availability state. +//! +//! When the namespace coordinator ([`super::schema`]) fails, whole-DB +//! sessions.db init must not fail with it — a corrupted Agent Org runtime +//! namespace must never take ordinary chat down. The startup hook records +//! the failure here via [`super::init_agent_org_schemas_scoped`], sessions.db +//! init proceeds, and every Agent Org store entry that would touch the +//! runtime namespace acquires its connection through [`runtime_connection`], +//! which returns a structured "agent-org runtime unavailable" error instead +//! of a raw missing-table SQL failure. +//! +//! Under `cfg(test)` the state is thread-local so parallel tests cannot +//! poison each other through the process-global; production uses a +//! process-wide static because the init hook and the command surfaces run +//! on different threads. + +use rusqlite::{ffi, Connection, Error as SqliteError, Result as SqliteResult}; + +/// Stable prefix of every gated error so command surfaces and the frontend +/// can recognize the scoped-degradation condition. +pub const AGENT_ORG_RUNTIME_UNAVAILABLE_PREFIX: &str = "agent-org runtime unavailable: "; + +#[cfg(not(test))] +static STATE: std::sync::RwLock> = std::sync::RwLock::new(None); + +#[cfg(test)] +thread_local! { + static STATE: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; +} + +/// Record that the Agent Org runtime namespace could not be initialized. +/// +/// Every subsequent [`runtime_connection`] fails with the structured +/// unavailable error until [`mark_agent_org_runtime_available`] clears it +/// (a later successful coordinator run, e.g. a rotated test sandbox). +pub fn mark_agent_org_runtime_unavailable(reason: impl Into) { + let reason = reason.into(); + #[cfg(not(test))] + { + *STATE.write().unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(reason); + } + #[cfg(test)] + STATE.with(|state| *state.borrow_mut() = Some(reason)); +} + +/// Clear the unavailable state after a successful coordinator run. +pub fn mark_agent_org_runtime_available() { + #[cfg(not(test))] + { + *STATE.write().unwrap_or_else(|poisoned| poisoned.into_inner()) = None; + } + #[cfg(test)] + STATE.with(|state| *state.borrow_mut() = None); +} + +/// The recorded coordinator failure, if the runtime namespace is unavailable. +pub fn agent_org_runtime_unavailable_reason() -> Option { + #[cfg(not(test))] + { + STATE + .read() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .clone() + } + #[cfg(test)] + STATE.with(|state| state.borrow().clone()) +} + +/// Fail with the structured unavailable error while the namespace is down. +pub fn ensure_agent_org_runtime_available() -> SqliteResult<()> { + match agent_org_runtime_unavailable_reason() { + None => Ok(()), + Some(reason) => Err(SqliteError::SqliteFailure( + ffi::Error::new(ffi::SQLITE_CANTOPEN), + Some(format!("{AGENT_ORG_RUNTIME_UNAVAILABLE_PREFIX}{reason}")), + )), + } +} + +/// Gated connection acquisition for every Agent Org store entry point. +/// +/// Identical to `database::db::get_connection()` while the runtime +/// namespace is healthy; returns the structured unavailable error without +/// touching SQLite once the coordinator has reported failure. +pub(crate) fn runtime_connection() -> SqliteResult { + ensure_agent_org_runtime_available()?; + database::db::get_connection() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gate_reports_structured_error_and_recovers() { + assert!(agent_org_runtime_unavailable_reason().is_none()); + ensure_agent_org_runtime_available().expect("available by default"); + + mark_agent_org_runtime_unavailable("boom: table missing"); + let error = runtime_connection().expect_err("gated while unavailable"); + let message = error.to_string(); + assert!( + message.contains("agent-org runtime unavailable: boom: table missing"), + "{message}" + ); + + mark_agent_org_runtime_available(); + assert!(agent_org_runtime_unavailable_reason().is_none()); + ensure_agent_org_runtime_available().expect("cleared"); + } +} diff --git a/src-tauri/crates/agent-core/src/core/coordination/mod.rs b/src-tauri/crates/agent-core/src/core/coordination/mod.rs index b9b9e3dff8..2674a4c5eb 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/mod.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/mod.rs @@ -27,21 +27,39 @@ pub mod agent_org_run_events; pub mod agent_org_runs; pub mod agent_org_tasks; pub mod agent_org_watchdog; +pub mod availability; pub mod child_done_wake; pub mod routine_scheduler; pub mod work_item_recovery; pub mod work_item_run_dispatcher; pub mod work_item_scheduler; +mod schema; + /// Initialize the complete durable Agent Org runtime schema in dependency /// order. Production and sandbox test entry points share this registry so a /// newly-added recovery table cannot silently exist in only one environment. pub fn init_agent_org_schemas(conn: &rusqlite::Connection) -> rusqlite::Result<()> { - agent_org_runs::init_schema(conn)?; - agent_inbox::init_schema(conn)?; - agent_org_tasks::init_schema(conn)?; - agent_org_plan_approvals::init_schema(conn)?; - agent_member_interventions::init_schema(conn)?; - agent_org_watchdog::init_schema(conn)?; - Ok(()) + schema::initialize(conn) +} + +/// Production startup entry: initialize the namespace, scoping any failure +/// to the Agent Org surface instead of failing whole sessions.db init. +/// +/// On coordinator failure the full diagnostic is logged at error level and +/// recorded in [`availability`]; every Agent Org store entry then returns +/// the structured "agent-org runtime unavailable" error while ordinary +/// chat (and the rest of sessions.db init) proceeds normally. +pub fn init_agent_org_schemas_scoped(conn: &rusqlite::Connection) { + match schema::initialize(conn) { + Ok(()) => availability::mark_agent_org_runtime_available(), + Err(error) => { + tracing::error!( + event = "agent_org_runtime_namespace_unavailable", + error = %error, + "Agent Org runtime namespace init failed; Agent Org features are disabled for this process while ordinary chat continues" + ); + availability::mark_agent_org_runtime_unavailable(error.to_string()); + } + } } diff --git a/src-tauri/crates/agent-core/src/core/coordination/schema.rs b/src-tauri/crates/agent-core/src/core/coordination/schema.rs new file mode 100644 index 0000000000..c2e4971e08 --- /dev/null +++ b/src-tauri/crates/agent-core/src/core/coordination/schema.rs @@ -0,0 +1,1140 @@ +//! Atomic ownership of the redesigned Agent Org private SQLite namespace. +//! +//! Old releases may recreate the retired names after a downgrade. Every new +//! process therefore retires the exact known legacy set again; there is no +//! one-time marker. Redesigned data is treated more conservatively: on a +//! manifest mismatch the namespace epoch stored in +//! `agent_org_runtime_meta` decides the outcome — an older epoch is the +//! sanctioned retire-and-recreate path for deliberate DDL changes, a newer +//! epoch (user rolled the binary back) and any unexplained mismatch fail +//! closed. Failing closed is scoped: the caller records the failure in +//! [`super::availability`] instead of failing whole sessions.db init. + +use std::collections::BTreeMap; + +use rusqlite::{ffi, Connection, Error as SqliteError, OptionalExtension, Result as SqliteResult}; + +use super::{ + agent_inbox, agent_member_interventions, agent_org_plan_approvals, agent_org_runs, + agent_org_tasks, agent_org_watchdog, +}; + +/// Version of the canonical runtime namespace as a whole. +/// +/// Bump this constant together with any DDL change to a runtime table. +/// A namespace whose stored epoch is older than the binary's is retired +/// and recreated (destructive by design — runtime state is rebuildable); +/// a namespace with a newer epoch fails closed so a rolled-back binary +/// never mangles data created by a newer release. +const SCHEMA_EPOCH: i64 = 1; + +const SCHEMA_EPOCH_KEY: &str = "schema_epoch"; + +const RUNTIME_TABLES: [&str; 14] = [ + "agent_org_runtime_meta", + "agent_org_runtime_runs", + "agent_org_runtime_run_progress", + "agent_org_runtime_member_materializations", + "agent_org_runtime_initial_inputs", + "agent_org_runtime_plan_approvals", + "agent_org_runtime_recovery_attempts", + "agent_org_runtime_tasks", + "agent_org_runtime_task_events", + "agent_org_runtime_task_schema_migrations", + "agent_org_runtime_inbox", + "agent_org_runtime_inbox_materializations", + "agent_org_runtime_inbox_delivery_resolutions", + "agent_org_runtime_member_interventions", +]; + +const LEGACY_TABLES: [&str; 13] = [ + "agent_org_runs", + "agent_org_run_progress", + "agent_org_member_materializations", + "agent_org_initial_inputs", + "agent_org_plan_approvals", + "agent_org_recovery_attempts", + "agent_org_tasks", + "agent_org_task_events", + "agent_org_task_run_schema_migrations", + "agent_inbox", + "agent_inbox_materializations", + "agent_inbox_delivery_resolutions", + "agent_member_interventions", +]; + +const RUNTIME_OBJECTS_QUERY: &str = "SELECT type, name, tbl_name, sql + FROM sqlite_master + WHERE sql IS NOT NULL + AND type IN ('table', 'index', 'trigger', 'view') + AND (name LIKE 'agent_org_runtime_%' OR tbl_name LIKE 'agent_org_runtime_%') + ORDER BY type, name"; + +const DROP_LEGACY_SCHEMA: &str = "DROP TABLE IF EXISTS agent_inbox_materializations; + DROP TABLE IF EXISTS agent_inbox_delivery_resolutions; + DROP TABLE IF EXISTS agent_inbox; + DROP TABLE IF EXISTS agent_org_task_events; + DROP TABLE IF EXISTS agent_org_task_run_schema_migrations; + DROP TABLE IF EXISTS agent_org_tasks; + DROP TABLE IF EXISTS agent_org_plan_approvals; + DROP TABLE IF EXISTS agent_org_recovery_attempts; + DROP TABLE IF EXISTS agent_member_interventions; + DROP TABLE IF EXISTS agent_org_initial_inputs; + DROP TABLE IF EXISTS agent_org_member_materializations; + DROP TABLE IF EXISTS agent_org_run_progress; + DROP TABLE IF EXISTS agent_org_runs;"; + +/// Epoch retire path: drop the canonical runtime namespace, children before +/// FK parents (`agent_org_runtime_runs` last). +const DROP_RUNTIME_SCHEMA: &str = "DROP TABLE IF EXISTS agent_org_runtime_meta; + DROP TABLE IF EXISTS agent_org_runtime_inbox_materializations; + DROP TABLE IF EXISTS agent_org_runtime_inbox_delivery_resolutions; + DROP TABLE IF EXISTS agent_org_runtime_inbox; + DROP TABLE IF EXISTS agent_org_runtime_task_events; + DROP TABLE IF EXISTS agent_org_runtime_task_schema_migrations; + DROP TABLE IF EXISTS agent_org_runtime_tasks; + DROP TABLE IF EXISTS agent_org_runtime_plan_approvals; + DROP TABLE IF EXISTS agent_org_runtime_recovery_attempts; + DROP TABLE IF EXISTS agent_org_runtime_member_interventions; + DROP TABLE IF EXISTS agent_org_runtime_initial_inputs; + DROP TABLE IF EXISTS agent_org_runtime_member_materializations; + DROP TABLE IF EXISTS agent_org_runtime_run_progress; + DROP TABLE IF EXISTS agent_org_runtime_runs;"; + +type SchemaManifest = BTreeMap<(String, String), (String, String)>; + +/// What `agent_org_runtime_meta` says about the namespace's epoch. +enum EpochReading { + /// The meta table itself does not exist: the namespace predates the + /// epoch mechanism. Treated as epoch 0, i.e. older than every binary + /// that carries this code. + PreEpoch, + /// A well-formed stored epoch. + Epoch(i64), + /// The meta table exists but the epoch row is missing or garbled. + Unreadable(String), +} + +/// Outcome of the namespace decision for this boot. +enum NamespaceAction { + /// No runtime tables at all: create the namespace from scratch. + Fresh, + /// Manifest and epoch both match this binary: nothing to do. + Canonical, + /// Stored epoch is older than [`SCHEMA_EPOCH`]: sanctioned + /// retire-and-recreate (destructive by design). + Recreate { from_epoch: i64 }, +} + +pub(super) fn initialize(conn: &Connection) -> SqliteResult<()> { + let expected = expected_manifest()?; + let tx = database::db::begin_immediate(conn)?; + let runtime_table_count = count_known_tables(&tx, &RUNTIME_TABLES)?; + + let action = if runtime_table_count == 0 { + NamespaceAction::Fresh + } else { + decide_existing_namespace_action(&tx, &expected)? + }; + + let legacy_table_count = count_known_tables(&tx, &LEGACY_TABLES)?; + let legacy_object_count = count_legacy_objects(&tx)?; + if legacy_table_count > 0 { + // Destructive by design: record what is about to be dropped so a + // data-loss report is diagnosable after the fact. Only paid when + // legacy tables actually exist — steady-state boots skip it. + let dropped = count_existing_table_rows(&tx, &LEGACY_TABLES)?; + log_destructive_table_drops("legacy_retirement", &dropped); + } + tx.execute_batch(DROP_LEGACY_SCHEMA)?; + + let (fresh, recreated_from_epoch) = match action { + NamespaceAction::Fresh => { + create_runtime_schema(&tx)?; + (true, None) + } + NamespaceAction::Canonical => (false, None), + NamespaceAction::Recreate { from_epoch } => { + let dropped = count_existing_table_rows(&tx, &RUNTIME_TABLES)?; + tracing::warn!( + event = "agent_org_runtime_namespace_retired", + from_epoch, + to_epoch = SCHEMA_EPOCH, + "retiring Agent Org runtime namespace from an older schema epoch; runtime state is recreated fresh" + ); + log_destructive_table_drops("epoch_recreate", &dropped); + tx.execute_batch(DROP_RUNTIME_SCHEMA)?; + create_runtime_schema(&tx)?; + (false, Some(from_epoch)) + } + }; + verify_manifest(&tx, &expected)?; + + // Receipt self-heal is O(existing data); it only needs to run after a + // boot that changed the namespace (fresh create, epoch recreate, or a + // legacy retirement). Canonical no-op boots skip it entirely. + let destructive_boot = fresh || recreated_from_epoch.is_some() || legacy_table_count > 0; + if destructive_boot { + agent_inbox::repair_dangling_materializations(&tx)?; + } + let unknown_objects = unknown_agent_org_objects(&tx)?; + tx.commit()?; + + // Dependency normalization has its own per-run transactions. Keep it + // outside the schema cutover transaction while preserving the historical + // init behavior for already-canonical runtime data. + agent_org_tasks::normalize_runtime_data(conn)?; + + if !unknown_objects.is_empty() { + tracing::warn!( + event = "agent_org_unknown_schema_objects_preserved", + objects = ?unknown_objects, + "preserved schema objects outside the exact legacy retirement registry" + ); + } + tracing::info!( + event = "agent_org_runtime_namespace_initialized", + legacy_table_count, + legacy_object_count, + fresh, + recreated_from_epoch, + schema_epoch = SCHEMA_EPOCH, + idempotent = !destructive_boot, + "initialized isolated Agent Org runtime schema" + ); + Ok(()) +} + +/// Decide what to do with a non-empty runtime namespace. +/// +/// The stored epoch is the authority whenever it is readable: +/// - epoch < binary → sanctioned retire-and-recreate (deliberate DDL +/// change; also covers pre-epoch namespaces without a meta table), +/// - epoch > binary → fail closed: the namespace was created by a newer +/// version and this rolled-back binary must not touch it, +/// - epoch == binary → the manifest must match exactly; any mismatch is +/// corruption and fails closed (scoped via `availability`), +/// - unreadable epoch row → corruption, fail closed. +fn decide_existing_namespace_action( + conn: &Connection, + expected: &SchemaManifest, +) -> SqliteResult { + let manifest_state = verify_manifest(conn, expected); + match read_schema_epoch(conn)? { + EpochReading::Epoch(epoch) if epoch > SCHEMA_EPOCH => Err(schema_error(format!( + "Agent Org runtime namespace was created by a newer version \ + (schema epoch {epoch}, this binary supports {SCHEMA_EPOCH}); refusing to touch it" + ))), + EpochReading::Epoch(epoch) if epoch < SCHEMA_EPOCH => { + Ok(NamespaceAction::Recreate { from_epoch: epoch }) + } + EpochReading::PreEpoch => Ok(NamespaceAction::Recreate { from_epoch: 0 }), + EpochReading::Epoch(_) => match manifest_state { + Ok(()) => Ok(NamespaceAction::Canonical), + Err(mismatch) => Err(mismatch), + }, + EpochReading::Unreadable(detail) => Err(schema_error(format!( + "unreadable Agent Org runtime schema epoch ({detail}); manifest {}", + match manifest_state { + Ok(()) => "matches".to_string(), + Err(mismatch) => format!("mismatch: {mismatch}"), + } + ))), + } +} + +fn read_schema_epoch(conn: &Connection) -> SqliteResult { + let meta_exists: bool = conn.query_row( + "SELECT EXISTS( + SELECT 1 FROM sqlite_master + WHERE type='table' AND name='agent_org_runtime_meta' + )", + [], + |row| row.get(0), + )?; + if !meta_exists { + return Ok(EpochReading::PreEpoch); + } + let value: Option = match conn + .query_row( + "SELECT value FROM agent_org_runtime_meta WHERE key=?1", + [SCHEMA_EPOCH_KEY], + |row| row.get(0), + ) + .optional() + { + Ok(value) => value, + // A meta table whose shape cannot even answer the query is as + // unreadable as a missing row. + Err(error) => return Ok(EpochReading::Unreadable(error.to_string())), + }; + Ok(match value { + None => EpochReading::Unreadable("schema_epoch row is missing".to_string()), + Some(raw) => match raw.trim().parse::() { + Ok(epoch) if epoch >= 0 => EpochReading::Epoch(epoch), + _ => EpochReading::Unreadable(format!( + "schema_epoch value {raw:?} is not a non-negative integer" + )), + }, + }) +} + +fn create_runtime_schema(conn: &Connection) -> SqliteResult<()> { + create_meta_schema(conn)?; + agent_org_runs::create_schema(conn)?; + agent_inbox::create_schema(conn)?; + agent_org_tasks::create_schema(conn)?; + agent_org_plan_approvals::create_schema(conn)?; + agent_member_interventions::create_schema(conn)?; + agent_org_watchdog::create_schema(conn) +} + +/// Namespace metadata, currently only the schema epoch. Lives inside the +/// canonical namespace (and therefore inside the manifest) so the epoch is +/// dropped and recreated together with the tables it describes. +fn create_meta_schema(conn: &Connection) -> SqliteResult<()> { + conn.execute_batch(&format!( + "CREATE TABLE IF NOT EXISTS agent_org_runtime_meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + INSERT OR REPLACE INTO agent_org_runtime_meta(key, value) + VALUES ('{SCHEMA_EPOCH_KEY}', '{SCHEMA_EPOCH}');" + )) +} + +fn expected_manifest() -> SqliteResult { + let expected = Connection::open_in_memory()?; + expected.execute_batch("PRAGMA foreign_keys=ON;")?; + create_runtime_schema(&expected)?; + read_manifest(&expected) +} + +fn verify_manifest(conn: &Connection, expected: &SchemaManifest) -> SqliteResult<()> { + let actual = read_manifest(conn)?; + if &actual == expected { + return Ok(()); + } + + let missing = expected + .keys() + .filter(|key| !actual.contains_key(*key)) + .cloned() + .collect::>(); + let unexpected = actual + .keys() + .filter(|key| !expected.contains_key(*key)) + .cloned() + .collect::>(); + let changed = expected + .iter() + .filter(|(key, value)| actual.get(*key).is_some_and(|item| item != *value)) + .map(|(key, _)| key.clone()) + .collect::>(); + Err(schema_error(format!( + "unknown Agent Org runtime schema; missing={missing:?}, unexpected={unexpected:?}, changed={changed:?}" + ))) +} + +fn read_manifest(conn: &Connection) -> SqliteResult { + let mut statement = conn.prepare(RUNTIME_OBJECTS_QUERY)?; + let rows = statement.query_map([], |row| { + let object_type: String = row.get(0)?; + let name: String = row.get(1)?; + let table_name: String = row.get(2)?; + let sql: String = row.get(3)?; + Ok(((object_type, name), (table_name, sql.trim().to_string()))) + })?; + rows.collect() +} + +/// Row counts for the subset of `names` that exist as tables. +/// +/// Used by every destructive path (legacy retirement, epoch recreate) right +/// before its `DROP TABLE`s so the log records exactly what was destroyed. +/// Never called on steady-state boots. +fn count_existing_table_rows( + conn: &Connection, + names: &[&str], +) -> SqliteResult> { + let mut counts = Vec::new(); + for name in names { + let exists: bool = conn.query_row( + "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1)", + [name], + |row| row.get(0), + )?; + if !exists { + continue; + } + let rows: i64 = + conn.query_row(&format!("SELECT COUNT(*) FROM \"{name}\""), [], |row| { + row.get(0) + })?; + counts.push(((*name).to_string(), rows)); + } + Ok(counts) +} + +fn log_destructive_table_drops(context: &'static str, counts: &[(String, i64)]) { + for (table, rows) in counts { + tracing::warn!( + event = "agent_org_destructive_table_drop", + context, + table = %table, + rows, + "dropping Agent Org table together with its rows" + ); + } +} + +fn count_known_tables(conn: &Connection, names: &[&str]) -> SqliteResult { + let mut statement = conn.prepare("SELECT name FROM sqlite_master WHERE type='table'")?; + let rows = statement.query_map([], |row| row.get::<_, String>(0))?; + let mut count = 0; + for row in rows { + if names.contains(&row?.as_str()) { + count += 1; + } + } + Ok(count) +} + +fn count_legacy_objects(conn: &Connection) -> SqliteResult { + let mut statement = conn.prepare( + "SELECT name, tbl_name FROM sqlite_master + WHERE type IN ('table', 'index', 'trigger', 'view')", + )?; + let rows = statement.query_map([], |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) + })?; + let mut count = 0; + for row in rows { + let (name, table_name) = row?; + if LEGACY_TABLES.contains(&name.as_str()) || LEGACY_TABLES.contains(&table_name.as_str()) { + count += 1; + } + } + Ok(count) +} + +fn unknown_agent_org_objects(conn: &Connection) -> SqliteResult> { + let mut statement = conn.prepare( + "SELECT name FROM sqlite_master + WHERE type IN ('table', 'index', 'trigger', 'view') + AND (name LIKE 'agent_org_%' OR name LIKE 'agent_inbox%' OR name LIKE 'agent_member_%') + ORDER BY name", + )?; + let rows = statement.query_map([], |row| row.get::<_, String>(0))?; + let mut unknown = Vec::new(); + for row in rows { + let name = row?; + let known_runtime = name.starts_with("sqlite_autoindex_agent_org_runtime_") + || read_known_runtime_object(conn, &name)?; + if !known_runtime && !LEGACY_TABLES.contains(&name.as_str()) { + unknown.push(name); + } + } + Ok(unknown) +} + +fn read_known_runtime_object(conn: &Connection, name: &str) -> SqliteResult { + conn.query_row( + "SELECT EXISTS( + SELECT 1 FROM sqlite_master + WHERE name=?1 + AND (name LIKE 'agent_org_runtime_%' OR tbl_name LIKE 'agent_org_runtime_%') + )", + [name], + |row| row.get(0), + ) +} + +fn schema_error(message: String) -> SqliteError { + SqliteError::SqliteFailure(ffi::Error::new(ffi::SQLITE_SCHEMA), Some(message)) +} + +#[cfg(test)] +mod tests { + use std::sync::{Arc, Barrier}; + use std::time::Duration; + + use super::*; + + fn connection() -> Connection { + let conn = Connection::open_in_memory().expect("in-memory SQLite"); + conn.execute_batch("PRAGMA foreign_keys=ON;") + .expect("enable foreign keys"); + conn + } + + fn object_exists(conn: &Connection, object_type: &str, name: &str) -> bool { + conn.query_row( + "SELECT EXISTS( + SELECT 1 FROM sqlite_master WHERE type=?1 AND name=?2 + )", + rusqlite::params![object_type, name], + |row| row.get(0), + ) + .expect("inspect schema object") + } + + fn row_count(conn: &Connection, table: &str) -> i64 { + conn.query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |row| { + row.get(0) + }) + .unwrap_or_else(|error| panic!("count {table}: {error}")) + } + + fn create_legacy_fixture(conn: &Connection, table_count: usize, unknown_column: bool) { + assert!(matches!(table_count, 5 | 9 | 11 | 13)); + let extra = if unknown_column { + ", local_develop_column BLOB" + } else { + "" + }; + conn.execute_batch(&format!( + "CREATE TABLE agent_org_runs (id INTEGER PRIMARY KEY, payload TEXT{extra}); + INSERT INTO agent_org_runs VALUES (1, 'legacy-0'{unknown_value}); + CREATE TABLE agent_org_tasks ( + id INTEGER PRIMARY KEY, org_run_id INTEGER NOT NULL, payload TEXT, + FOREIGN KEY(org_run_id) REFERENCES agent_org_runs(id) ON DELETE CASCADE + ); + INSERT INTO agent_org_tasks VALUES (1, 1, 'legacy-1'); + CREATE TABLE agent_org_task_events ( + id INTEGER PRIMARY KEY, task_id INTEGER NOT NULL, payload TEXT, + FOREIGN KEY(task_id) REFERENCES agent_org_tasks(id) ON DELETE CASCADE + ); + INSERT INTO agent_org_task_events VALUES (1, 1, 'legacy-2'); + CREATE TABLE agent_inbox ( + id INTEGER PRIMARY KEY, org_run_id INTEGER NOT NULL, payload TEXT, + FOREIGN KEY(org_run_id) REFERENCES agent_org_runs(id) ON DELETE CASCADE + ); + INSERT INTO agent_inbox VALUES (1, 1, 'legacy-3'); + CREATE TABLE agent_member_interventions ( + id INTEGER PRIMARY KEY, org_run_id INTEGER NOT NULL, payload TEXT, + FOREIGN KEY(org_run_id) REFERENCES agent_org_runs(id) ON DELETE CASCADE + ); + INSERT INTO agent_member_interventions VALUES (1, 1, 'legacy-4');", + unknown_value = if unknown_column { ", x'0102'" } else { "" }, + )) + .expect("create five-table legacy fixture"); + if table_count >= 9 { + conn.execute_batch( + "CREATE TABLE agent_org_run_progress ( + id INTEGER PRIMARY KEY, org_run_id INTEGER NOT NULL, payload TEXT, + FOREIGN KEY(org_run_id) REFERENCES agent_org_runs(id) ON DELETE CASCADE + ); + INSERT INTO agent_org_run_progress VALUES (1, 1, 'legacy-5'); + CREATE TABLE agent_org_plan_approvals ( + id INTEGER PRIMARY KEY, org_run_id INTEGER NOT NULL, payload TEXT, + FOREIGN KEY(org_run_id) REFERENCES agent_org_runs(id) ON DELETE CASCADE + ); + INSERT INTO agent_org_plan_approvals VALUES (1, 1, 'legacy-6'); + CREATE TABLE agent_org_recovery_attempts ( + id INTEGER PRIMARY KEY, org_run_id INTEGER NOT NULL, payload TEXT, + FOREIGN KEY(org_run_id) REFERENCES agent_org_runs(id) ON DELETE CASCADE + ); + INSERT INTO agent_org_recovery_attempts VALUES (1, 1, 'legacy-7'); + CREATE TABLE agent_inbox_materializations ( + id INTEGER PRIMARY KEY, inbox_id INTEGER NOT NULL, payload TEXT, + FOREIGN KEY(inbox_id) REFERENCES agent_inbox(id) ON DELETE CASCADE + ); + INSERT INTO agent_inbox_materializations VALUES (1, 1, 'legacy-8');", + ) + .expect("create nine-table legacy fixture"); + } + if table_count >= 11 { + conn.execute_batch( + "CREATE TABLE agent_inbox_delivery_resolutions ( + id INTEGER PRIMARY KEY, inbox_id INTEGER NOT NULL, payload TEXT, + FOREIGN KEY(inbox_id) REFERENCES agent_inbox(id) ON DELETE CASCADE + ); + INSERT INTO agent_inbox_delivery_resolutions VALUES (1, 1, 'legacy-9'); + CREATE TABLE agent_org_task_run_schema_migrations ( + id INTEGER PRIMARY KEY, org_run_id INTEGER NOT NULL, payload TEXT, + FOREIGN KEY(org_run_id) REFERENCES agent_org_runs(id) ON DELETE CASCADE + ); + INSERT INTO agent_org_task_run_schema_migrations VALUES (1, 1, 'legacy-10');", + ) + .expect("create eleven-table legacy fixture"); + } + if table_count >= 13 { + conn.execute_batch( + "CREATE TABLE agent_org_member_materializations ( + id INTEGER PRIMARY KEY, org_run_id INTEGER NOT NULL, payload TEXT, + FOREIGN KEY(org_run_id) REFERENCES agent_org_runs(id) ON DELETE CASCADE + ); + INSERT INTO agent_org_member_materializations VALUES (1, 1, 'legacy-11'); + CREATE TABLE agent_org_initial_inputs ( + id INTEGER PRIMARY KEY, org_run_id INTEGER NOT NULL, payload TEXT, + FOREIGN KEY(org_run_id) REFERENCES agent_org_runs(id) ON DELETE CASCADE + ); + INSERT INTO agent_org_initial_inputs VALUES (1, 1, 'legacy-12');", + ) + .expect("create thirteen-table legacy fixture"); + } + conn.execute_batch( + "CREATE INDEX idx_legacy_agent_org_runs_payload ON agent_org_runs(payload); + CREATE TRIGGER trg_legacy_agent_org_runs_touch + AFTER UPDATE ON agent_org_runs BEGIN SELECT 1; END;", + ) + .expect("legacy index and trigger"); + } + + fn seed_shared_sentinels(conn: &Connection) { + conn.execute_batch( + "CREATE TABLE agent_sessions ( + id TEXT PRIMARY KEY, session_id TEXT UNIQUE, payload BLOB, org_member_id TEXT + ); + CREATE TABLE agent_messages ( + id TEXT PRIMARY KEY, session_id TEXT, payload BLOB + ); + CREATE TABLE code_sessions ( + id TEXT PRIMARY KEY, session_id TEXT UNIQUE, payload BLOB, org_member_id TEXT + ); + CREATE TABLE session_turn_intents (id TEXT PRIMARY KEY, payload BLOB, org_run_id TEXT); + CREATE TABLE projects (id TEXT PRIMARY KEY, payload BLOB); + CREATE TABLE work_items (id TEXT PRIMARY KEY, payload BLOB); + CREATE TABLE routines (id TEXT PRIMARY KEY, payload BLOB); + CREATE TABLE usage_events (id TEXT PRIMARY KEY, payload BLOB); + INSERT INTO agent_sessions VALUES ('rust', 'rust-session', x'000102', 'member-a'); + INSERT INTO agent_messages VALUES ('message', 'rust-session', x'030405'); + INSERT INTO code_sessions VALUES ('cli', 'cli-session', x'060708', 'member-b'); + INSERT INTO session_turn_intents VALUES ('intent', x'090A0B', 'run-a'); + INSERT INTO projects VALUES ('project', x'0C0D0E'); + INSERT INTO work_items VALUES ('work-item', x'0F1011'); + INSERT INTO routines VALUES ('routine', x'121314'); + INSERT INTO usage_events VALUES ('usage', x'151617');", + ) + .expect("shared sentinels"); + } + + fn shared_sentinel_fingerprint(conn: &Connection) -> Vec<(String, String)> { + [ + "agent_sessions", + "agent_messages", + "code_sessions", + "session_turn_intents", + "projects", + "work_items", + "routines", + "usage_events", + ] + .into_iter() + .map(|table| { + let fingerprint = conn + .query_row(&format!("SELECT hex(payload) FROM {table}"), [], |row| { + row.get::<_, String>(0) + }) + .unwrap_or_else(|error| panic!("fingerprint {table}: {error}")); + (table.to_string(), fingerprint) + }) + .collect() + } + + #[test] + fn retires_every_historical_namespace_shape_without_touching_shared_data() { + for (table_count, unknown_column) in + [(5, false), (9, false), (11, false), (13, false), (13, true)] + { + let conn = connection(); + create_legacy_fixture(&conn, table_count, unknown_column); + seed_shared_sentinels(&conn); + let shared_before = shared_sentinel_fingerprint(&conn); + + initialize(&conn).expect("retire legacy namespace"); + + for table in LEGACY_TABLES { + assert!(!object_exists(&conn, "table", table), "retained {table}"); + } + for table in RUNTIME_TABLES { + assert!(object_exists(&conn, "table", table), "missing {table}"); + let expected_rows = if table == "agent_org_runtime_meta" { + 1 // the schema_epoch row + } else { + 0 + }; + assert_eq!( + row_count(&conn, table), + expected_rows, + "fresh {table} row count" + ); + } + assert!(!object_exists( + &conn, + "index", + "idx_legacy_agent_org_runs_payload" + )); + assert!(!object_exists( + &conn, + "trigger", + "trg_legacy_agent_org_runs_touch" + )); + assert_eq!(shared_sentinel_fingerprint(&conn), shared_before); + } + } + + #[test] + fn repeated_downgrade_cleanup_preserves_the_complete_canonical_runtime() { + let conn = connection(); + initialize(&conn).expect("fresh runtime"); + conn.execute_batch( + "INSERT INTO agent_org_runtime_runs ( + id, org_id, coordinator_agent_id, root_session_id, + org_snapshot_json, entry_mode, status, created_at, updated_at + ) VALUES ( + 'team-a', 'org-a', 'coordinator-a', 'root-a', '{\"team\":\"A\"}', + 'standalone_session', 'idle', '2026-08-01T00:00:00Z', '2026-08-01T00:00:00Z' + ); + INSERT INTO agent_org_runtime_run_progress + (org_run_id, work_revision, completion_requested, updated_at) + VALUES ('team-a', 7, 1, '2026-08-01T00:00:00Z'); + INSERT INTO agent_org_runtime_member_materializations + (org_run_id, member_id, agent_id, generation, session_id, + authority_class, status, created_at, updated_at) + VALUES ('team-a', 'member-a', 'agent-a', 1, 'session-a', + 'starting', 'succeeded', '2026-08-01T00:00:00Z', '2026-08-01T00:00:00Z'); + INSERT INTO agent_org_runtime_initial_inputs + (org_run_id, turn_intent_id, message_id, content, payload_json, + status, created_at, updated_at) + VALUES ('team-a', 'turn-a', 'message-a', 'hello', '{}', + 'dispatched', '2026-08-01T00:00:00Z', '2026-08-01T00:00:00Z'); + INSERT INTO agent_org_runtime_tasks + (id, org_run_id, subject, status, created_at, updated_at) + VALUES ('task-a', 'team-a', 'Task A', 'completed', + '2026-08-01T00:00:00Z', '2026-08-01T00:00:00Z'); + INSERT INTO agent_org_runtime_task_events + (id, org_run_id, task_id, event_type, created_at) + VALUES ('event-a', 'team-a', 'task-a', 'completed', '2026-08-01T00:00:00Z'); + INSERT INTO agent_org_runtime_task_schema_migrations + (name, org_run_id, applied_at) + VALUES ('canonical_blocked_by_v1', 'team-a', '2026-08-01T00:00:00Z'); + INSERT INTO agent_org_runtime_inbox + (recipient_agent_id, recipient_member_id, sender_agent_id, + sender_member_id, org_run_id, payload_kind, payload_json, created_at, read_at) + VALUES ('agent-a', 'member-a', 'coordinator-a', 'coordinator', 'team-a', + 'message', '{}', '2026-08-01T00:00:00Z', '2026-08-01T00:00:01Z'); + INSERT INTO agent_org_runtime_inbox_materializations + (inbox_id, session_id, transcript_message_id, transcript_intent_id, materialized_at) + VALUES (1, 'session-a', 'message-a', 'turn-a', '2026-08-01T00:00:01Z'); + INSERT INTO agent_org_runtime_inbox_delivery_resolutions + (inbox_id, org_run_id, resolution_kind, resolved_by_member_id, + reason, created_at) + VALUES (2, 'team-a', 'cancelled', 'coordinator', 'done', '2026-08-01T00:00:02Z'); + INSERT INTO agent_org_runtime_plan_approvals + (approval_id, plan_revision_id, request_id, org_run_id, source_task_id, + source_member_id, source_session_id, root_session_id, policy, status, + plan_title, plan_path, plan_content, created_at) + VALUES ('approval-a', 'revision-a', 'request-a', 'team-a', 'task-a', + 'member-a', 'session-a', 'root-a', 'coordinator', 'approved', + 'Plan', '/tmp/plan-a', '# plan', '2026-08-01T00:00:00Z'); + INSERT INTO agent_org_runtime_recovery_attempts + (org_run_id, action_kind, target_key, reason_fingerprint, attempts, + next_allowed_at, updated_at) + VALUES ('team-a', 'member_rewake', 'member-a', 'fingerprint', 1, + '2026-08-01T00:00:00Z', '2026-08-01T00:00:00Z'); + INSERT INTO agent_org_runtime_member_interventions + (org_run_id, member_id, agent_id, session_id, status, entered_at, + last_user_activity_at, resume_after) + VALUES ('team-a', 'member-a', 'agent-a', 'session-a', 'user_intervention', + '2026-08-01T00:00:00Z', '2026-08-01T00:00:00Z', '2099-08-01T00:00:00Z');", + ) + .expect("canonical Team A fixture"); + let before = RUNTIME_TABLES + .into_iter() + .map(|table| (table, row_count(&conn, table))) + .collect::>(); + + for _ in 0..2 { + create_legacy_fixture(&conn, 13, true); + initialize(&conn).expect("re-upgrade cleanup"); + assert_eq!( + RUNTIME_TABLES + .into_iter() + .map(|table| (table, row_count(&conn, table))) + .collect::>(), + before + ); + for table in LEGACY_TABLES { + assert!(!object_exists(&conn, "table", table)); + } + } + + let snapshot: String = conn + .query_row( + "SELECT org_snapshot_json FROM agent_org_runtime_runs WHERE id='team-a'", + [], + |row| row.get(0), + ) + .expect("preserved snapshot"); + assert_eq!(snapshot, "{\"team\":\"A\"}"); + } + + #[test] + fn partial_or_unknown_runtime_schema_fails_closed_before_legacy_cleanup() { + for mutate in ["partial", "changed", "extra_index"] { + let conn = connection(); + initialize(&conn).expect("canonical runtime"); + conn.execute_batch("CREATE TABLE agent_org_runs (sentinel TEXT); INSERT INTO agent_org_runs VALUES ('legacy');") + .expect("legacy sentinel"); + match mutate { + "partial" => conn + .execute_batch("DROP TABLE agent_org_runtime_initial_inputs;") + .expect("make partial schema"), + "changed" => { + conn.execute_batch( + "DROP TABLE agent_org_runtime_member_interventions; + CREATE TABLE agent_org_runtime_member_interventions (sentinel TEXT);", + ) + .expect("make changed schema"); + } + "extra_index" => conn + .execute_batch( + "CREATE INDEX idx_agent_org_runtime_unknown + ON agent_org_runtime_runs(updated_at);", + ) + .expect("make unknown index"), + _ => unreachable!(), + } + + let error = initialize(&conn).expect_err("unknown runtime must fail closed"); + assert!( + error.to_string().contains("Agent Org runtime schema"), + "{error}" + ); + assert_eq!(row_count(&conn, "agent_org_runs"), 1); + } + } + + fn seed_minimal_run(conn: &Connection) { + conn.execute_batch( + "INSERT INTO agent_org_runtime_runs ( + id, org_id, coordinator_agent_id, root_session_id, + org_snapshot_json, entry_mode, status, created_at, updated_at + ) VALUES ( + 'epoch-run', 'org-a', 'coordinator-a', 'root-a', '{}', + 'standalone_session', 'idle', '2026-08-01T00:00:00Z', '2026-08-01T00:00:00Z' + );", + ) + .expect("seed runtime run row"); + } + + fn stored_epoch(conn: &Connection) -> String { + conn.query_row( + "SELECT value FROM agent_org_runtime_meta WHERE key='schema_epoch'", + [], + |row| row.get(0), + ) + .expect("read stored schema epoch") + } + + #[test] + fn older_epoch_namespace_is_retired_and_recreated() { + // (a) Explicit older epoch with a manifest mismatch: the sanctioned + // retire-and-recreate path for a deliberate DDL change. + // (b) Pre-epoch namespace (no meta table at all): treated as epoch 0. + for variant in ["older_epoch", "pre_epoch"] { + let conn = connection(); + initialize(&conn).expect("canonical runtime"); + seed_minimal_run(&conn); + seed_shared_sentinels(&conn); + let shared_before = shared_sentinel_fingerprint(&conn); + match variant { + "older_epoch" => conn + .execute_batch( + "UPDATE agent_org_runtime_meta SET value='0' WHERE key='schema_epoch'; + DROP TABLE agent_org_runtime_initial_inputs;", + ) + .expect("simulate an older-epoch namespace"), + "pre_epoch" => conn + .execute_batch("DROP TABLE agent_org_runtime_meta;") + .expect("simulate a pre-epoch namespace"), + _ => unreachable!(), + } + + initialize(&conn).expect("epoch upgrade must recreate the namespace"); + + for table in RUNTIME_TABLES { + assert!( + object_exists(&conn, "table", table), + "{variant}: missing {table}" + ); + } + assert_eq!( + row_count(&conn, "agent_org_runtime_runs"), + 0, + "{variant}: recreate is destructive by design" + ); + assert_eq!(stored_epoch(&conn), SCHEMA_EPOCH.to_string()); + assert_eq!(shared_sentinel_fingerprint(&conn), shared_before); + verify_manifest(&conn, &expected_manifest().expect("expected manifest")) + .expect("canonical manifest after epoch recreate"); + } + } + + #[test] + fn newer_epoch_namespace_fails_closed_with_rollback_diagnostic() { + let conn = connection(); + initialize(&conn).expect("canonical runtime"); + seed_minimal_run(&conn); + conn.execute( + "UPDATE agent_org_runtime_meta SET value='2' WHERE key='schema_epoch'", + [], + ) + .expect("simulate a namespace created by a newer version"); + + let error = initialize(&conn).expect_err("rolled-back binary must fail closed"); + assert!(error.to_string().contains("newer version"), "{error}"); + + // Nothing was touched: the newer-version data survives intact. + assert_eq!(row_count(&conn, "agent_org_runtime_runs"), 1); + assert_eq!(stored_epoch(&conn), "2"); + } + + #[test] + fn unreadable_epoch_fails_closed_as_corruption() { + for mutate in [ + // Meta table intact but the epoch row is gone. + "DELETE FROM agent_org_runtime_meta WHERE key='schema_epoch';", + // Epoch row present but garbled. + "UPDATE agent_org_runtime_meta SET value='not-a-number' WHERE key='schema_epoch';", + ] { + let conn = connection(); + initialize(&conn).expect("canonical runtime"); + seed_minimal_run(&conn); + conn.execute_batch(mutate).expect("corrupt the epoch row"); + + let error = initialize(&conn).expect_err("unreadable epoch must fail closed"); + assert!( + error.to_string().contains("unreadable Agent Org runtime schema epoch"), + "{error}" + ); + assert_eq!(row_count(&conn, "agent_org_runtime_runs"), 1); + } + } + + #[test] + fn create_failure_rolls_back_every_legacy_drop() { + let conn = connection(); + create_legacy_fixture(&conn, 13, false); + conn.execute_batch("CREATE VIEW agent_org_runtime_runs AS SELECT 1 AS id;") + .expect("runtime name conflict"); + + initialize(&conn).expect_err("runtime create must fail"); + + for table in LEGACY_TABLES { + assert!( + object_exists(&conn, "table", table), + "rollback lost {table}" + ); + assert_eq!(row_count(&conn, table), 1); + } + assert!(object_exists(&conn, "view", "agent_org_runtime_runs")); + } + + #[test] + fn unknown_agent_org_objects_are_preserved() { + let conn = connection(); + conn.execute_batch( + "CREATE TABLE agent_org_local_experiment (sentinel TEXT); + INSERT INTO agent_org_local_experiment VALUES ('keep');", + ) + .expect("unknown table"); + + initialize(&conn).expect("initialize around unknown object"); + + assert_eq!(row_count(&conn, "agent_org_local_experiment"), 1); + } + + #[test] + fn concurrent_initializers_serialize_to_one_canonical_schema() { + let directory = tempfile::tempdir().expect("temporary database directory"); + let path = directory.path().join("sessions.db"); + let barrier = Arc::new(Barrier::new(2)); + let handles = (0..2) + .map(|_| { + let path = path.clone(); + let barrier = Arc::clone(&barrier); + std::thread::spawn(move || { + let conn = Connection::open(path).expect("open shared SQLite database"); + conn.busy_timeout(Duration::from_secs(5)) + .expect("set busy timeout"); + conn.execute_batch("PRAGMA foreign_keys=ON;") + .expect("enable foreign keys"); + barrier.wait(); + initialize(&conn) + }) + }) + .collect::>(); + + for handle in handles { + handle + .join() + .expect("initializer thread") + .expect("initialize"); + } + + let conn = Connection::open(path).expect("reopen shared database"); + verify_manifest(&conn, &expected_manifest().expect("expected manifest")) + .expect("canonical manifest after concurrent init"); + assert_eq!( + count_known_tables(&conn, &RUNTIME_TABLES).unwrap(), + RUNTIME_TABLES.len() + ); + } + + #[test] + fn destructive_drop_counting_reports_existing_tables_with_their_rows() { + let conn = connection(); + create_legacy_fixture(&conn, 5, false); + + let counts = + count_existing_table_rows(&conn, &LEGACY_TABLES).expect("count legacy tables"); + + // Only the five existing fixture tables are reported; each carries + // its seeded single row. Missing tables never appear. + assert_eq!(counts.len(), 5); + for (table, rows) in &counts { + assert!( + LEGACY_TABLES.contains(&table.as_str()), + "unexpected {table}" + ); + assert_eq!(*rows, 1, "{table} row count"); + } + assert!(counts + .iter() + .any(|(table, _)| table == "agent_org_runs")); + assert!(!counts + .iter() + .any(|(table, _)| table == "agent_org_run_progress")); + + // Retirement wipes them; a rerun reports nothing to destroy. + initialize(&conn).expect("retire legacy fixture"); + assert!(count_existing_table_rows(&conn, &LEGACY_TABLES) + .expect("count after retirement") + .is_empty()); + } + + #[test] + fn scoped_init_degrades_to_unavailable_without_failing_db_init() { + use crate::coordination::availability; + + let conn = connection(); + initialize(&conn).expect("canonical runtime"); + conn.execute_batch("DROP TABLE agent_org_runtime_initial_inputs;") + .expect("corrupt the runtime namespace"); + + // The production startup entry: coordinator failure is scoped, not + // propagated into whole sessions.db init failure. + crate::coordination::init_agent_org_schemas_scoped(&conn); + + let reason = + availability::agent_org_runtime_unavailable_reason().expect("failure recorded"); + assert!(reason.contains("Agent Org runtime schema"), "{reason}"); + + // Connection-layer init proceeds: later initializers still run DDL + // on the same connection, so ordinary chat keeps working. + conn.execute_batch("CREATE TABLE ordinary_chat_sentinel (id INTEGER PRIMARY KEY);") + .expect("whole-DB init continues past the coordinator failure"); + + // Every Agent Org store entry acquires its connection through the + // gate and receives the structured unavailable error. + let error = availability::runtime_connection().expect_err("gated store entry"); + assert!( + error + .to_string() + .contains(availability::AGENT_ORG_RUNTIME_UNAVAILABLE_PREFIX), + "{error}" + ); + + // A later successful coordinator run restores availability. + let healthy = connection(); + crate::coordination::init_agent_org_schemas_scoped(&healthy); + assert!(availability::agent_org_runtime_unavailable_reason().is_none()); + } + + /// Guard for the per-boot O(data) fixes: a canonical no-op boot must + /// execute exactly as many SQL statements on a database holding 2000 + /// inbox rows + 2000 receipts + 2000 tasks as on an empty one. + /// Statement-count equality is robust where time thresholds are not. + #[test] + fn no_op_boot_statement_count_is_independent_of_data_scale() { + use std::cell::Cell; + + thread_local! { + static STATEMENTS: Cell = const { Cell::new(0) }; + } + fn count_statement(_sql: &str) { + STATEMENTS.with(|counter| counter.set(counter.get() + 1)); + } + fn measured_no_op_boot(conn: &mut Connection) -> usize { + conn.trace(Some(count_statement)); + STATEMENTS.with(|counter| counter.set(0)); + initialize(conn).expect("canonical no-op boot"); + conn.trace(None); + STATEMENTS.with(|counter| counter.get()) + } + + // Baseline: fresh create, one settle boot, then a measured no-op. + let mut empty = connection(); + initialize(&empty).expect("fresh init"); + initialize(&empty).expect("settle boot"); + let empty_statements = measured_no_op_boot(&mut empty); + + // Seeded: identical boot sequence with 20 runs, 2000 unread inbox + // rows, 2000 materialization receipts, and 2000 tasks (100 per run, + // inside the per-run limit) present. + let mut seeded = connection(); + initialize(&seeded).expect("fresh init"); + seeded + .execute_batch( + "WITH RECURSIVE seq(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n<20) + INSERT INTO agent_org_runtime_runs ( + id, org_id, coordinator_agent_id, root_session_id, + org_snapshot_json, entry_mode, status, created_at, updated_at + ) + SELECT 'run-'||printf('%02d', n), 'org-scale', 'coordinator-a', 'root-'||n, + '{}', 'standalone_session', 'idle', + '2026-08-01T00:00:00Z', '2026-08-01T00:00:00Z' + FROM seq; + WITH RECURSIVE seq(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n<2000) + INSERT INTO agent_org_runtime_inbox ( + recipient_agent_id, recipient_member_id, sender_agent_id, + sender_member_id, org_run_id, payload_kind, payload_json, + created_at, read_at + ) + SELECT 'agent-a', 'member-a', 'coordinator-a', 'coordinator', + 'run-'||printf('%02d', 1+(n%20)), 'message', '{}', + '2026-08-01T00:00:00Z', NULL + FROM seq; + WITH RECURSIVE seq(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n<2000) + INSERT INTO agent_org_runtime_inbox_materializations ( + inbox_id, session_id, transcript_message_id, + transcript_intent_id, materialized_at + ) + SELECT n, 'session-'||n, 'message-'||n, 'turn-'||n, + '2026-08-01T00:00:01Z' + FROM seq; + WITH RECURSIVE seq(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n<2000) + INSERT INTO agent_org_runtime_tasks ( + id, org_run_id, subject, description, status, + blocks_json, blocked_by_json, created_at, updated_at + ) + SELECT 'task-'||n, 'run-'||printf('%02d', 1+(n%20)), 'Task '||n, '', + 'pending', '[]', '[]', + '2026-08-01T00:00:00Z', '2026-08-01T00:00:00Z' + FROM seq;", + ) + .expect("seed 20 runs, 2000 inbox rows, 2000 receipts, 2000 tasks"); + initialize(&seeded).expect("settle boot marks every run's normalization"); + let seeded_statements = measured_no_op_boot(&mut seeded); + + assert!(empty_statements > 0, "trace hook must observe the boot"); + assert_eq!( + seeded_statements, empty_statements, + "canonical no-op boot must execute a constant statement count \ + regardless of stored data volume" + ); + } +} diff --git a/src-tauri/crates/agent-core/src/core/definitions/orgs.rs b/src-tauri/crates/agent-core/src/core/definitions/orgs.rs index 3f97738ec0..d65c106b82 100644 --- a/src-tauri/crates/agent-core/src/core/definitions/orgs.rs +++ b/src-tauri/crates/agent-core/src/core/definitions/orgs.rs @@ -2,16 +2,15 @@ use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; -use std::fs::{File, OpenOptions}; +use std::fs::File; use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::Path; #[cfg(not(test))] use std::sync::OnceLock; use std::sync::{Arc, Mutex}; -use std::time::{SystemTime, UNIX_EPOCH}; use tracing::{error, info, warn}; -use app_paths::agent_orgs as storage_path; +use app_paths::agent_org_definitions as storage_path; use key_vault::ModelType; #[cfg(not(test))] @@ -397,7 +396,6 @@ enum LoadOutcome { orgs: Vec, quarantined: Vec, }, - LegacyReset, Blocked(String), } @@ -417,12 +415,12 @@ impl Default for AgentOrgsStore { impl AgentOrgsStore { pub fn new() -> Self { + retire_legacy_definitions_file(); let path = storage_path(); let (mut orgs, quarantined, persistence_blocked, should_persist) = match load_from_disk(&path) { LoadOutcome::Missing => (Vec::new(), Vec::new(), None, true), LoadOutcome::Loaded { orgs, quarantined } => (orgs, quarantined, None, false), - LoadOutcome::LegacyReset => (Vec::new(), Vec::new(), None, true), LoadOutcome::Blocked(message) => { error!("[agent-orgs] {}", message); (Vec::new(), Vec::new(), Some(message), false) @@ -1153,29 +1151,6 @@ fn load_from_disk(path: &Path) -> LoadOutcome { return LoadOutcome::Blocked(format!("Failed to parse {}: {}", path.display(), err)); } }; - - // The legacy sniff only runs on version-less/array files. A file that - // declares the current schema version is authoritative: nested - // `children`/`hierarchyMode` keys inside field values must never - // trigger a destructive backup-and-reset of a valid v2 file. - let declared_current_version = value - .as_object() - .and_then(|object| object.get("schemaVersion")) - .and_then(serde_json::Value::as_u64) - == Some(u64::from(AGENT_ORGS_FILE_SCHEMA_VERSION)); - if !declared_current_version && (value.is_array() || contains_legacy_hierarchy(&value)) { - match backup_legacy_file(path, &bytes) { - Ok(backup_path) => { - warn!( - "[agent-orgs] Backed up legacy recursive definitions to {} before reset", - backup_path.display() - ); - return LoadOutcome::LegacyReset; - } - Err(err) => return LoadOutcome::Blocked(err), - } - } - let file: AgentOrgDefinitionsFile = match serde_json::from_value(value) { Ok(file) => file, Err(err) => { @@ -1248,71 +1223,156 @@ fn load_from_disk(path: &Path) -> LoadOutcome { LoadOutcome::Loaded { orgs, quarantined } } -fn contains_legacy_hierarchy(value: &serde_json::Value) -> bool { - match value { - serde_json::Value::Object(object) => { - object.contains_key("children") - || object.contains_key("hierarchyMode") - || object.values().any(contains_legacy_hierarchy) - } - serde_json::Value::Array(values) => values.iter().any(contains_legacy_hierarchy), - _ => false, +/// Strict, all-or-nothing parse of a definitions file. +/// +/// Used by the legacy-file adoption path: a pre-rename file is adopted only +/// when the envelope and every definition inside it are valid; otherwise +/// the caller retires the old file instead of adopting it. Boot-time +/// loading of the canonical file goes through `load_from_disk`, which +/// quarantines invalid entries individually instead of rejecting the file. +pub(crate) fn parse_definitions_content( + bytes: &[u8], + path: &Path, +) -> Result, String> { + let value: serde_json::Value = serde_json::from_slice(bytes) + .map_err(|err| format!("Failed to parse {}: {}", path.display(), err))?; + let file: AgentOrgDefinitionsFile = serde_json::from_value(value).map_err(|err| { + format!( + "Unrecognized Agent Org definitions file {}: {}", + path.display(), + err + ) + })?; + if file.schema_version != AGENT_ORGS_FILE_SCHEMA_VERSION { + return Err(format!( + "Unsupported Agent Org definitions schema version {} in {}", + file.schema_version, + path.display() + )); } -} - -fn backup_legacy_file(path: &Path, bytes: &[u8]) -> Result { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|err| { + let mut definitions: Vec = Vec::with_capacity(file.definitions.len()); + for (index, raw) in file.definitions.into_iter().enumerate() { + let org = serde_json::from_value::(raw).map_err(|err| { format!( - "System clock error while backing up legacy definitions: {}", - err + "Invalid Agent Org definitions in {}: definitions[{index}] is not a valid Agent Org: {err}", + path.display() ) - })? - .as_millis(); - let file_name = path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("agent-orgs.json"); - let parent = path.parent().unwrap_or_else(|| Path::new(".")); - for suffix in 0..1000u16 { - let suffix_text = if suffix == 0 { - String::new() - } else { - format!("-{}", suffix) - }; - let backup_path = parent.join(format!( - "{}.legacy-{}{}.bak", - file_name, timestamp, suffix_text - )); - match OpenOptions::new() - .write(true) - .create_new(true) - .open(&backup_path) - { - Ok(mut file) => { - file.write_all(bytes) - .and_then(|_| file.sync_all()) - .map_err(|err| { - format!( - "Failed to write legacy Agent Org backup {}: {}", - backup_path.display(), - err - ) - })?; - return Ok(backup_path); + })?; + definitions.push(org); + } + canonicalize_and_validate_definitions(&mut definitions).map_err(|err| { + format!( + "Invalid Agent Org definitions in {}: {}", + path.display(), + err + ) + })?; + Ok(definitions) +} + +/// Retire — or, when possible, adopt — the pre-rename `agent-orgs.json`. +/// +/// Both the old and the new file carry the same schema_version=2 envelope, +/// so a valid old file is user data, not garbage. When the canonical file +/// does not exist yet and the old file parses as a valid v2 envelope, move +/// it to the canonical path (preserving user-defined Teams) instead of +/// destroying it unparsed. An invalid/legacy-shaped old file, or an old +/// file alongside an existing canonical file, is retired (deleted) as +/// before. +fn retire_legacy_definitions_file() { + let legacy_path = app_paths::agent_orgs(); + if !legacy_path.exists() { + return; + } + + let canonical_path = storage_path(); + if !canonical_path.exists() { + match try_adopt_legacy_definitions_file(&legacy_path, &canonical_path) { + Ok(()) => { + info!( + event = "agent_org_legacy_definitions_adopted", + from = %legacy_path.display(), + to = %canonical_path.display(), + "adopted valid v2 Agent Org definitions from the retired path" + ); + return; } - Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue, - Err(err) => { - return Err(format!( - "Failed to create legacy Agent Org backup {}: {}", - backup_path.display(), - err - )); + Err(AdoptError::NotAdoptable(reason)) => { + info!( + event = "agent_org_legacy_definitions_not_adoptable", + path = %legacy_path.display(), + reason = %reason, + "retired Agent Org definitions file is not a valid v2 envelope; retiring it" + ); + // fall through to retirement below + } + Err(AdoptError::Io(reason)) => { + // Leave the old file in place so the next startup retries; + // deleting it here would destroy the user's Teams. + warn!( + event = "agent_org_legacy_definitions_adoption_failed", + path = %legacy_path.display(), + error = %reason, + "could not adopt retired Agent Org definitions file; leaving it for a later retry" + ); + return; } } } - Err("Could not allocate a unique legacy Agent Org backup path".to_string()) + + match std::fs::remove_file(&legacy_path) { + Ok(()) => info!( + event = "agent_org_legacy_definitions_retired", + path = %legacy_path.display(), + "removed retired Agent Org definitions file" + ), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => warn!( + event = "agent_org_legacy_definitions_retirement_failed", + path = %legacy_path.display(), + error = %err, + "could not remove retired Agent Org definitions file; canonical store remains isolated" + ), + } +} + +enum AdoptError { + /// The old file is unreadable-as-data or not a valid v2 envelope — + /// retire it like any other stale artifact. + NotAdoptable(String), + /// The old file holds valid definitions but moving it failed — leave + /// it in place and retry on a later startup. + Io(String), +} + +fn try_adopt_legacy_definitions_file( + legacy_path: &Path, + canonical_path: &Path, +) -> Result<(), AdoptError> { + let bytes = std::fs::read(legacy_path) + .map_err(|err| AdoptError::NotAdoptable(format!("read failed: {err}")))?; + parse_definitions_content(&bytes, legacy_path).map_err(AdoptError::NotAdoptable)?; + + if let Some(parent) = canonical_path.parent() { + std::fs::create_dir_all(parent) + .map_err(|err| AdoptError::Io(format!("create directory failed: {err}")))?; + } + if std::fs::rename(legacy_path, canonical_path).is_ok() { + return Ok(()); + } + // Rename can fail across filesystems or on contended Windows handles; + // fall back to copy + delete, keeping the old file on copy failure. + std::fs::copy(legacy_path, canonical_path) + .map_err(|err| AdoptError::Io(format!("copy failed: {err}")))?; + if let Err(err) = std::fs::remove_file(legacy_path) { + warn!( + event = "agent_org_legacy_definitions_cleanup_failed", + path = %legacy_path.display(), + error = %err, + "adopted Agent Org definitions but could not remove the old file" + ); + } + Ok(()) } fn save_to_disk( @@ -1723,21 +1783,139 @@ mod tests { } #[test] - fn legacy_array_is_backed_up_before_reset() { + fn invalid_legacy_file_is_still_retired_and_new_builtins_are_created() { let _sandbox = test_helpers::test_env::sandbox(); - let path = storage_path(); - std::fs::create_dir_all(path.parent().unwrap()).unwrap(); - let legacy = br#"[{"id":"old","children":[]}]"#; - std::fs::write(&path, legacy).unwrap(); + let new_path = storage_path(); + let legacy_path = app_paths::agent_orgs(); + let unrelated_path = legacy_path.parent().unwrap().join("agent-definitions.json"); + std::fs::create_dir_all(new_path.parent().unwrap()).unwrap(); + std::fs::write(&legacy_path, b"not even JSON").unwrap(); + std::fs::write(&unrelated_path, b"unrelated sentinel").unwrap(); + let store = AgentOrgsStore::new(); + + assert!(store.get(DEFAULT_SDE_TEMPLATE_TEAM_ID).is_ok()); + assert!(!legacy_path.exists()); + assert!(new_path.exists()); + assert_eq!( + std::fs::read(unrelated_path).unwrap(), + b"unrelated sentinel" + ); + } + + #[test] + fn valid_v2_legacy_file_is_adopted_preserving_custom_teams() { + let _sandbox = test_helpers::test_env::sandbox(); + let new_path = storage_path(); + let legacy_path = app_paths::agent_orgs(); + std::fs::create_dir_all(legacy_path.parent().unwrap()).unwrap(); + + let mut org = custom_org(&["alice", "bob"]); + org.id = "user-team".to_string(); + org.name = "User Team".to_string(); + org.additional_task_graph_writer_member_ids = vec!["alice".to_string()]; + let file = AgentOrgDefinitionsFile { + schema_version: AGENT_ORGS_FILE_SCHEMA_VERSION, + definitions: vec![serde_json::to_value(&org).expect("encode custom Team")], + }; + std::fs::write( + &legacy_path, + serde_json::to_vec_pretty(&file).expect("serialize v2 envelope"), + ) + .expect("write byte-compatible legacy file"); + assert!(!new_path.exists()); + + let store = AgentOrgsStore::new(); + + // The user's Team survived the rename cutover… + assert_eq!(store.get("user-team").expect("adopted custom Team"), org); + // …alongside the reconciled built-in templates… + assert!(store.get(DEFAULT_SDE_TEMPLATE_TEAM_ID).is_ok()); + // …and the old path is gone while the canonical one exists. + assert!(!legacy_path.exists()); + assert!(new_path.is_file()); + + // A restart round-trips the adopted content from the canonical path. + let restarted = AgentOrgsStore::new(); + assert_eq!(restarted.get("user-team").expect("persisted Team"), org); + } + + #[test] + fn legacy_v2_file_next_to_existing_canonical_file_is_retired_not_adopted() { + let _sandbox = test_helpers::test_env::sandbox(); + let store = AgentOrgsStore::new(); + let mut kept = custom_org(&["alice"]); + kept.id = "kept-team".to_string(); + kept.name = "Kept Team".to_string(); + store.insert(kept.clone()).expect("persist canonical Team"); + let new_path = storage_path(); + let new_bytes = std::fs::read(&new_path).expect("canonical bytes"); + + // A valid v2 envelope at the old path must NOT displace the + // canonical file once it exists. + let mut stale = custom_org(&["carol"]); + stale.id = "stale-team".to_string(); + stale.name = "Stale Team".to_string(); + let file = AgentOrgDefinitionsFile { + schema_version: AGENT_ORGS_FILE_SCHEMA_VERSION, + definitions: vec![serde_json::to_value(&stale).expect("encode stale Team")], + }; + let legacy_path = app_paths::agent_orgs(); + std::fs::write( + &legacy_path, + serde_json::to_vec_pretty(&file).expect("serialize stale envelope"), + ) + .expect("write stale legacy file"); + + let restarted = AgentOrgsStore::new(); + + assert!(!legacy_path.exists(), "old file is deleted"); + assert_eq!( + std::fs::read(&new_path).expect("canonical bytes after restart"), + new_bytes, + "canonical file is untouched" + ); + assert_eq!(restarted.get("kept-team").expect("kept Team"), kept); + assert!(restarted.get("stale-team").is_err()); + } + + #[test] + fn downgrade_recreated_legacy_file_is_deleted_without_changing_new_bytes() { + let _sandbox = test_helpers::test_env::sandbox(); + let store = AgentOrgsStore::new(); + let mut org = custom_org(&["alice", "bob"]); + org.id = "preserved-org".to_string(); + org.name = "Preserved Org".to_string(); + org.additional_task_graph_writer_member_ids = vec!["alice".to_string()]; + org.member_communication_links = vec![MemberCommunicationLink::canonical("alice", "bob")]; + store.insert(org.clone()).expect("persist canonical Team"); + let new_path = storage_path(); + let new_bytes = std::fs::read(&new_path).expect("canonical bytes"); + let legacy_path = app_paths::agent_orgs(); + std::fs::write(&legacy_path, br#"[{"id":"downgrade-team"}]"#) + .expect("downgrade legacy file"); + + let restarted = AgentOrgsStore::new(); + + assert!(!legacy_path.exists()); + assert_eq!( + std::fs::read(&new_path).expect("new bytes after cleanup"), + new_bytes + ); + assert_eq!(restarted.get(&org.id).expect("preserved Team"), org); + } + + #[test] + fn legacy_cleanup_failure_never_redirects_the_store_to_the_old_path() { + let _sandbox = test_helpers::test_env::sandbox(); + let legacy_path = app_paths::agent_orgs(); + std::fs::create_dir_all(&legacy_path).expect("directory blocks file cleanup"); + + let store = AgentOrgsStore::new(); + + assert!(legacy_path.is_dir()); + assert!(storage_path().is_file()); assert!(store.get(DEFAULT_SDE_TEMPLATE_TEAM_ID).is_ok()); - let backups = std::fs::read_dir(path.parent().unwrap()) - .unwrap() - .filter_map(Result::ok) - .filter(|entry| entry.file_name().to_string_lossy().contains("legacy-")) - .collect::>(); - assert_eq!(backups.len(), 1); - assert_eq!(std::fs::read(backups[0].path()).unwrap(), legacy); } #[test] @@ -1854,8 +2032,9 @@ mod tests { let path = storage_path(); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); // A v2 envelope whose definition carries a nested `children` key - // (e.g. hostile or drifted data). The legacy sniff must not - // back up and reset the file; the entry is quarantined instead. + // (e.g. hostile or drifted data). The file must never be backed + // up and reset (the legacy sniff that once did this is gone); + // the invalid entry is quarantined instead. let file = format!( r#"{{"schemaVersion":2,"definitions":[ {{"id":"odd-org","name":"Odd Org","role":"c","agentId":"{sde}", @@ -1894,8 +2073,13 @@ mod tests { org.member_communication_links = vec![MemberCommunicationLink::canonical("alice", "carol")]; store.insert(org.clone()).expect("persist custom Team"); + let bytes_before = std::fs::read(storage_path()).expect("canonical bytes before restart"); let restarted = AgentOrgsStore::new(); assert_eq!(restarted.get(&org.id).expect("reloaded Team"), org); + assert_eq!( + std::fs::read(storage_path()).expect("canonical bytes after restart"), + bytes_before + ); } #[test] diff --git a/src-tauri/crates/agent-core/src/core/session/persistence/messages.rs b/src-tauri/crates/agent-core/src/core/session/persistence/messages.rs index 9807cfa092..fb5a47852c 100644 --- a/src-tauri/crates/agent-core/src/core/session/persistence/messages.rs +++ b/src-tauri/crates/agent-core/src/core/session/persistence/messages.rs @@ -48,7 +48,7 @@ pub fn load_agent_org_inbox_transcript_materializations( receipt.transcript_message_id, receipt.transcript_intent_id, message.content - FROM agent_inbox_materializations receipt + FROM agent_org_runtime_inbox_materializations receipt LEFT JOIN agent_messages message ON message.id=receipt.transcript_message_id AND message.session_id=receipt.session_id @@ -112,7 +112,7 @@ pub fn materialize_agent_org_inbox_transcript( let mut stmt = tx .prepare( "SELECT session_id, transcript_message_id, transcript_intent_id - FROM agent_inbox_materializations WHERE inbox_id=?1", + FROM agent_org_runtime_inbox_materializations WHERE inbox_id=?1", ) .map_err(|err| err.to_string())?; for inbox_id in inbox_ids { @@ -140,7 +140,7 @@ pub fn materialize_agent_org_inbox_transcript( let unread_count = { let mut stmt = tx - .prepare("SELECT read_at FROM agent_inbox WHERE id=?1") + .prepare("SELECT read_at FROM agent_org_runtime_inbox WHERE id=?1") .map_err(|err| err.to_string())?; let mut count = 0usize; for inbox_id in inbox_ids { @@ -213,7 +213,7 @@ pub fn materialize_agent_org_inbox_transcript( { let mut stmt = tx .prepare( - "INSERT INTO agent_inbox_materializations + "INSERT INTO agent_org_runtime_inbox_materializations (inbox_id, session_id, transcript_message_id, transcript_intent_id, materialized_at) VALUES (?1, ?2, ?3, ?4, ?5)", ) diff --git a/src-tauri/crates/agent-core/src/core/session/persistence/sidebar.rs b/src-tauri/crates/agent-core/src/core/session/persistence/sidebar.rs index 996c629dcd..b2bee552b2 100644 --- a/src-tauri/crates/agent-core/src/core/session/persistence/sidebar.rs +++ b/src-tauri/crates/agent-core/src/core/session/persistence/sidebar.rs @@ -58,7 +58,7 @@ fn list_agent_sessions_page( Some(true) => { "AND EXISTS ( SELECT 1 - FROM agent_org_runs r + FROM agent_org_runtime_runs r WHERE r.root_session_id = s.session_id )" } @@ -66,7 +66,7 @@ fn list_agent_sessions_page( "AND s.org_member_id IS NULL AND NOT EXISTS ( SELECT 1 - FROM agent_org_runs r + FROM agent_org_runtime_runs r WHERE r.root_session_id = s.session_id )" } @@ -206,7 +206,7 @@ mod tests { ensure_runtime_schemas(); let conn = database::db::get_connection().expect("test sqlite connection"); conn.execute( - "INSERT INTO agent_org_runs ( + "INSERT INTO agent_org_runtime_runs ( id, org_id, coordinator_agent_id, @@ -404,7 +404,7 @@ mod tests { AND s.org_member_id IS NULL AND NOT EXISTS ( SELECT 1 - FROM agent_org_runs r + FROM agent_org_runtime_runs r WHERE r.root_session_id = s.session_id ) ORDER BY s.updated_at DESC, s.session_id DESC @@ -423,7 +423,7 @@ mod tests { "session page did not use ordered pin/type index:\n{details}" ); assert!( - details.contains("idx_agent_org_runs_root_session"), + details.contains("idx_agent_org_runtime_runs_root_session"), "root membership probe did not use root-session index:\n{details}" ); } diff --git a/src-tauri/crates/agent-core/src/core/session/turn/processor/inbox_drain/tests.rs b/src-tauri/crates/agent-core/src/core/session/turn/processor/inbox_drain/tests.rs index d9cba7b3c6..eeffb6df1c 100644 --- a/src-tauri/crates/agent-core/src/core/session/turn/processor/inbox_drain/tests.rs +++ b/src-tauri/crates/agent-core/src/core/session/turn/processor/inbox_drain/tests.rs @@ -1096,7 +1096,7 @@ fn materialized_batch_replay_delivers_only_rows_that_arrived_later() { let receipt_count: i64 = database::db::get_connection() .expect("receipt connection") .query_row( - "SELECT COUNT(*) FROM agent_inbox_materializations", + "SELECT COUNT(*) FROM agent_org_runtime_inbox_materializations", [], |row| row.get(0), ) @@ -1277,7 +1277,7 @@ fn shutdown_notification_failure_leaves_source_unread_and_retries_before_deliver let conn = database::db::get_connection().expect("db"); conn.execute_batch( "CREATE TRIGGER fail_member_terminated - BEFORE INSERT ON agent_inbox + BEFORE INSERT ON agent_org_runtime_inbox WHEN NEW.payload_kind='member_terminated' BEGIN SELECT RAISE(ABORT, 'forced member termination insert failure'); diff --git a/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/inbox_repair.rs b/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/inbox_repair.rs index ede831b8d2..34ca272626 100644 --- a/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/inbox_repair.rs +++ b/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/inbox_repair.rs @@ -266,7 +266,7 @@ mod tests { text: "Preserve this original message".into(), }; conn.execute( - "INSERT INTO agent_inbox ( + "INSERT INTO agent_org_runtime_inbox ( recipient_agent_id, recipient_member_id, sender_agent_id, sender_member_id, org_run_id, payload_kind, payload_json, created_at @@ -376,7 +376,7 @@ mod tests { let fixture = fixture(); let conn = get_connection().expect("test sqlite connection"); conn.execute( - "UPDATE agent_org_runs SET status='archived' WHERE id=?1", + "UPDATE agent_org_runtime_runs SET status='archived' WHERE id=?1", params![&fixture.run_id], ) .expect("archive run"); diff --git a/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/send_message/persistence.rs b/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/send_message/persistence.rs index 517003592b..be8fbb0bf0 100644 --- a/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/send_message/persistence.rs +++ b/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/send_message/persistence.rs @@ -118,7 +118,7 @@ pub(super) fn persist_ordinary_message_if_running( .map_err(|err| ToolError::ExecutionFailed(err.to_string()))?; let run_status: Option = tx .query_row( - "SELECT status FROM agent_org_runs WHERE id=?1", + "SELECT status FROM agent_org_runtime_runs WHERE id=?1", params![run_id], |row| row.get(0), ) diff --git a/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/send_message/tests.rs b/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/send_message/tests.rs index 3317786382..0b5270b093 100644 --- a/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/send_message/tests.rs +++ b/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/send_message/tests.rs @@ -132,7 +132,7 @@ fn init_inbox_schema() -> test_helpers::test_env::SandboxGuard { .expect("CLI session schema"); let now = chrono::Utc::now().to_rfc3339(); conn.execute( - "INSERT INTO agent_org_runs ( + "INSERT INTO agent_org_runtime_runs ( id, org_id, coordinator_agent_id, root_session_id, entry_mode, status, created_at, updated_at ) VALUES ('run-1', 'org-1', 'agent-coord', 'root-1', @@ -361,7 +361,7 @@ async fn ordinary_message_does_not_create_unread_work_after_run_is_archived() { let _sandbox = init_inbox_schema(); let conn = database::db::get_connection().expect("test sqlite connection"); conn.execute( - "UPDATE agent_org_runs SET status='archived' WHERE id='run-1'", + "UPDATE agent_org_runtime_runs SET status='archived' WHERE id='run-1'", [], ) .expect("archive run"); diff --git a/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/task_tests.rs b/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/task_tests.rs index 5b6ab2beb4..26a445fdee 100644 --- a/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/task_tests.rs +++ b/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/agent_org/task_tests.rs @@ -259,7 +259,7 @@ fn task_tools_sandbox() -> test_env::SandboxGuard { ), ] { conn.execute( - "INSERT OR IGNORE INTO agent_org_runs + "INSERT OR IGNORE INTO agent_org_runtime_runs (id, org_id, coordinator_agent_id, root_session_id, entry_mode, status, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, 'standalone_session', 'running', ?5, ?5)", @@ -2186,18 +2186,18 @@ async fn completing_legacy_blocks_only_edge_dispatches_downstream_once() { // downstream.blocked_by, so only a raw fixture can preserve it. let conn = database::db::get_connection().expect("test sqlite connection"); conn.execute( - "UPDATE agent_org_tasks SET blocks_json='[\"legacy-downstream\"]' + "UPDATE agent_org_runtime_tasks SET blocks_json='[\"legacy-downstream\"]' WHERE org_run_id='run-tools-1' AND id='legacy-upstream'", [], ) .expect("seed legacy blocks edge"); conn.execute( - "UPDATE agent_org_tasks SET blocked_by_json='[]' + "UPDATE agent_org_runtime_tasks SET blocked_by_json='[]' WHERE org_run_id='run-tools-1' AND id='legacy-downstream'", [], ) .expect("keep downstream legacy-only"); - conn.execute("DELETE FROM agent_inbox", []) + conn.execute("DELETE FROM agent_org_runtime_inbox", []) .expect("remove create-time assignment noise"); let update = TaskUpdateTool::new(Arc::clone(&coordinator)); @@ -2804,7 +2804,7 @@ fn seed_task_list_current_turn_quiescence_fixture(materialize_inbox: bool) -> i6 let now = chrono::Utc::now().to_rfc3339(); let conn = database::db::get_connection().expect("test sqlite connection"); conn.execute( - "UPDATE agent_org_runs + "UPDATE agent_org_runtime_runs SET root_session_id='root-tools-1', status='running', updated_at=?2 WHERE id=?1", rusqlite::params!["run-tools-1", &now], @@ -3037,7 +3037,7 @@ async fn task_list_current_turn_projection_fails_closed_for_wrong_identity_or_re database::db::get_connection() .expect("test sqlite connection") .execute( - "DELETE FROM agent_inbox_materializations WHERE inbox_id=?1", + "DELETE FROM agent_org_runtime_inbox_materializations WHERE inbox_id=?1", rusqlite::params![inbox_id], ) .expect("remove the receipt to exercise fail-closed validation"); @@ -3075,7 +3075,7 @@ async fn task_list_completion_certificate_blocks_while_reviewer_is_running() { let now = chrono::Utc::now().to_rfc3339(); let conn = database::db::get_connection().unwrap(); conn.execute( - "INSERT OR REPLACE INTO agent_org_runs + "INSERT OR REPLACE INTO agent_org_runtime_runs (id, org_id, coordinator_agent_id, root_session_id, entry_mode, status, created_at, updated_at) VALUES ('run-tools-1', 'org-tools-1', 'coord-1', 'root-tools-1', 'standalone_session', 'running', ?1, ?1)", rusqlite::params![now], @@ -3196,7 +3196,7 @@ async fn task_list_surfaces_corrupt_task_data_without_false_empty_completion() { let now = chrono::Utc::now().to_rfc3339(); let conn = database::db::get_connection().unwrap(); conn.execute( - "INSERT OR REPLACE INTO agent_org_runs + "INSERT OR REPLACE INTO agent_org_runtime_runs (id, org_id, coordinator_agent_id, root_session_id, entry_mode, status, created_at, updated_at) VALUES ('run-tools-1', 'org-tools-1', 'coord-1', 'root-tools-1', 'standalone_session', 'running', ?1, ?1)", rusqlite::params![&now], @@ -3226,7 +3226,7 @@ async fn task_list_surfaces_corrupt_task_data_without_false_empty_completion() { }) .unwrap(); conn.execute( - "UPDATE agent_org_tasks SET blocks_json='not-json' WHERE id='corrupt-task'", + "UPDATE agent_org_runtime_tasks SET blocks_json='not-json' WHERE id='corrupt-task'", [], ) .unwrap(); diff --git a/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/context_builders.rs b/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/context_builders.rs index 4cd85a665e..d853167fa9 100644 --- a/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/context_builders.rs +++ b/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/context_builders.rs @@ -129,30 +129,14 @@ pub fn build_agent_definitions_context() -> Option { /// Build a compact listing of agent organizations. /// -/// Same fail-soft policy as [`build_agent_definitions_context`]: -/// invalid on-disk JSON is logged via `warn!` and the section is -/// silently dropped from the prompt rather than aborting the launch. +/// Uses the canonical process store so prompt assembly observes exactly the +/// same versioned definitions and fail-closed validation as settings, launch, +/// Work Items, and Routines. pub fn build_agent_orgs_context() -> Option { - let orgs_path = app_paths::agent_orgs(); - if !orgs_path.exists() { - return None; - } - let content = std::fs::read_to_string(&orgs_path) + let orgs = crate::definitions::orgs::orgs_store() + .list() .map_err(|err| { - warn!( - "[agent] read agent organizations context from {}: {}; section skipped", - orgs_path.display(), - err - ); - }) - .ok()?; - let orgs: Vec = serde_json::from_str(&content) - .map_err(|err| { - warn!( - "[agent] parse agent organizations context from {}: {}; section skipped", - orgs_path.display(), - err - ); + warn!("[agent] load canonical Agent Org context: {err}; section skipped"); }) .ok()?; if orgs.is_empty() { diff --git a/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/member_idle.rs b/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/member_idle.rs index 5393dfd7c6..a30b258765 100644 --- a/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/member_idle.rs +++ b/src-tauri/crates/agent-core/src/core/tools/impls/orchestration/member_idle.rs @@ -212,7 +212,7 @@ mod tests { crate::coordination::agent_org_runs::init_schema(conn).expect("Agent Org run schema"); let now = chrono::Utc::now().to_rfc3339(); conn.execute( - "INSERT INTO agent_org_runs ( + "INSERT INTO agent_org_runtime_runs ( id, org_id, coordinator_agent_id, root_session_id, entry_mode, status, created_at, updated_at ) VALUES (?1, 'org-1', 'coord', 'root-1', 'build', ?2, ?3, ?3)", diff --git a/src-tauri/crates/agent-core/src/foundation/persistence/db_helpers/messages/cleanup.rs b/src-tauri/crates/agent-core/src/foundation/persistence/db_helpers/messages/cleanup.rs index 00e2fe9f89..11cd659b71 100644 --- a/src-tauri/crates/agent-core/src/foundation/persistence/db_helpers/messages/cleanup.rs +++ b/src-tauri/crates/agent-core/src/foundation/persistence/db_helpers/messages/cleanup.rs @@ -49,13 +49,13 @@ pub fn clear_messages(prefix: &str, session_id: &str) -> SqliteResult { with_sessions_writer(|| { let mut conn = get_connection()?; let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; - if prefix == "agent" && table_exists(&tx, "agent_inbox_materializations")? { + if prefix == "agent" && table_exists(&tx, "agent_org_runtime_inbox_materializations")? { // Rewind/clear removes the durable transcript but intentionally // leaves the source Inbox row unread. Deleting its receipt lets // the next wake materialize the input again instead of pointing // forever at a transcript that no longer exists. tx.execute( - "DELETE FROM agent_inbox_materializations WHERE session_id=?1", + "DELETE FROM agent_org_runtime_inbox_materializations WHERE session_id=?1", [session_id], )?; } @@ -87,9 +87,9 @@ pub fn truncate_messages_from_sequence( with_sessions_writer(|| { let mut conn = get_connection()?; let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; - if prefix == "agent" && table_exists(&tx, "agent_inbox_materializations")? { + if prefix == "agent" && table_exists(&tx, "agent_org_runtime_inbox_materializations")? { tx.execute( - "DELETE FROM agent_inbox_materializations + "DELETE FROM agent_org_runtime_inbox_materializations WHERE session_id=?1 AND transcript_message_id IN ( SELECT id FROM agent_messages diff --git a/src-tauri/crates/agent-core/src/foundation/persistence/db_helpers/mod.rs b/src-tauri/crates/agent-core/src/foundation/persistence/db_helpers/mod.rs index 78b6139569..fe248e9e96 100644 --- a/src-tauri/crates/agent-core/src/foundation/persistence/db_helpers/mod.rs +++ b/src-tauri/crates/agent-core/src/foundation/persistence/db_helpers/mod.rs @@ -190,7 +190,7 @@ fn delete_session_rows_with_connection( let receipts_exist: bool = conn.query_row( "SELECT EXISTS( SELECT 1 FROM sqlite_master - WHERE type='table' AND name='agent_inbox_materializations' + WHERE type='table' AND name='agent_org_runtime_inbox_materializations' )", [], |row| row.get(0), @@ -199,7 +199,7 @@ fn delete_session_rows_with_connection( // Keep the source Inbox rows unread while atomically removing // receipts for transcript rows deleted by this cascade. conn.execute( - "DELETE FROM agent_inbox_materializations WHERE session_id=?1", + "DELETE FROM agent_org_runtime_inbox_materializations WHERE session_id=?1", [session_id], )?; } diff --git a/src-tauri/crates/agent-core/src/foundation/tool_infra/project/execution.rs b/src-tauri/crates/agent-core/src/foundation/tool_infra/project/execution.rs index 93d24f5964..65fcb1d8a8 100644 --- a/src-tauri/crates/agent-core/src/foundation/tool_infra/project/execution.rs +++ b/src-tauri/crates/agent-core/src/foundation/tool_infra/project/execution.rs @@ -184,19 +184,6 @@ fn parse_agent_defs_for_execution( }) } -fn parse_agent_orgs_for_execution( - content: &str, - path: &std::path::Path, -) -> Result, String> { - serde_json::from_str(content).map_err(|err| { - format!( - "parse agent organizations for work-item launch from {}: {}", - path.display(), - err - ) - }) -} - /// `#[doc(hidden)]` — only the `app::api::agent::test::core` debug /// route calls this, via the `agent_core::tool_infra::*` re-export. #[cfg(debug_assertions)] @@ -208,9 +195,9 @@ pub fn debug_parse_work_item_launch_sources(kind: &str, content: &str) -> Result std::path::Path::new("work-item-agent-definitions-test.json"), ) .map(|items| items.len()), - "agent_orgs" => parse_agent_orgs_for_execution( - content, - std::path::Path::new("work-item-agent-orgs-test.json"), + "agent_orgs" => crate::definitions::orgs::parse_definitions_content( + content.as_bytes(), + std::path::Path::new("work-item-agent-org-definitions-test.json"), ) .map(|items| items.len()), _ => Err(format!("unknown work-item launch source kind: {kind}")), @@ -261,39 +248,14 @@ fn resolve_agent_def_id_from_assignee( let Some(org_id) = frontmatter.assignee.as_deref().filter(|s| !s.is_empty()) else { return Ok(None); }; - let orgs_path = app_paths::agent_orgs(); - if !orgs_path.exists() { - return Err(format!( - "agent organization '{}' is referenced by the work item but {} does not exist", - org_id, - orgs_path.display() - )); - } - let content = std::fs::read_to_string(&orgs_path).map_err(|err| { - format!( - "read agent organizations for work-item launch from {}: {}", - orgs_path.display(), - err - ) - })?; - let orgs = parse_agent_orgs_for_execution(&content, &orgs_path)?; - let org = orgs - .iter() - .find(|org| org.id == org_id) - .ok_or_else(|| { - format!( - "agent organization '{}' is referenced by the work item but was not found in {}", - org_id, - orgs_path.display() - ) - })?; + let org = crate::definitions::orgs::orgs_store().get(org_id)?; if org.agent_id.is_empty() { return Err(format!( "agent organization '{}' has an empty agent_id and cannot launch a work item", org_id )); } - Ok(Some(org.agent_id.clone())) + Ok(Some(org.agent_id)) } _ => Ok(None), } @@ -878,7 +840,7 @@ pub async fn launch_phase_session( #[cfg(test)] mod tests { - use super::{parse_agent_defs_for_execution, parse_agent_orgs_for_execution}; + use super::parse_agent_defs_for_execution; #[test] fn parse_agent_defs_for_execution_reports_invalid_json() { @@ -890,15 +852,4 @@ mod tests { "got: {err}" ); } - - #[test] - fn parse_agent_orgs_for_execution_reports_invalid_json() { - let err = parse_agent_orgs_for_execution("{ invalid", std::path::Path::new("orgs.json")) - .unwrap_err(); - - assert!( - err.contains("parse agent organizations for work-item launch"), - "got: {err}" - ); - } } diff --git a/src-tauri/crates/agent-core/src/lifecycle.rs b/src-tauri/crates/agent-core/src/lifecycle.rs index c4c4b02104..72987e3eea 100644 --- a/src-tauri/crates/agent-core/src/lifecycle.rs +++ b/src-tauri/crates/agent-core/src/lifecycle.rs @@ -804,15 +804,8 @@ mod tests { fn ensure_runtime_schemas() { let conn = database::db::get_connection().expect("test sqlite connection"); crate::persistence::test_schema::ensure_agent_sessions_schema(&conn); - crate::coordination::agent_org_runs::init_schema(&conn).expect("agent org runs schema"); - crate::coordination::agent_org_tasks::init_schema(&conn).expect("agent org tasks schema"); - crate::coordination::agent_org_plan_approvals::init_schema(&conn) - .expect("agent org plan approvals schema"); - crate::coordination::agent_member_interventions::init_schema(&conn) - .expect("agent member interventions schema"); - crate::coordination::agent_org_watchdog::init_schema(&conn) - .expect("agent org recovery schema"); - crate::coordination::agent_inbox::init_schema(&conn).expect("agent inbox schema"); + crate::coordination::init_agent_org_schemas(&conn) + .expect("complete Agent Org runtime schema"); conn.execute_batch( "CREATE TABLE IF NOT EXISTS code_sessions ( session_id TEXT PRIMARY KEY, diff --git a/src-tauri/crates/agent-core/src/state/commands/session/message/org_wake.rs b/src-tauri/crates/agent-core/src/state/commands/session/message/org_wake.rs index fbeeff52e1..5f2222d355 100644 --- a/src-tauri/crates/agent-core/src/state/commands/session/message/org_wake.rs +++ b/src-tauri/crates/agent-core/src/state/commands/session/message/org_wake.rs @@ -23,7 +23,7 @@ pub(super) fn promote_agent_org_wake_session_to_running( "WITH RECURSIVE run_anchor(root_session_id) AS ( SELECT root_session_id - FROM agent_org_runs + FROM agent_org_runtime_runs WHERE id=?4 AND status=?5 AND root_session_id IS NOT NULL ), descendants(session_id) AS ( @@ -33,7 +33,7 @@ pub(super) fn promote_agent_org_wake_session_to_running( FROM agent_sessions child JOIN descendants parent ON child.parent_session_id=parent.session_id WHERE NOT EXISTS ( - SELECT 1 FROM agent_org_runs nested + SELECT 1 FROM agent_org_runtime_runs nested WHERE nested.id<>?4 AND nested.root_session_id=child.session_id ) @@ -64,7 +64,7 @@ pub(super) fn promote_agent_org_wake_session_to_running( ) AND NOT EXISTS ( SELECT 1 - FROM agent_member_interventions intervention + FROM agent_org_runtime_member_interventions intervention WHERE intervention.org_run_id=?4 AND intervention.member_id=CASE WHEN agent_sessions.session_id=(SELECT root_session_id FROM run_anchor) @@ -106,7 +106,7 @@ pub(super) fn promote_agent_org_direct_session_to_running( let run_status = conn .query_row( - "SELECT status FROM agent_org_runs WHERE id=?1", + "SELECT status FROM agent_org_runtime_runs WHERE id=?1", [run_id], |row| row.get::<_, String>(0), ) @@ -165,13 +165,13 @@ pub(super) fn resolve_agent_org_wake_mode( .prepare( "WITH delivery_candidates AS ( SELECT id, payload_kind, payload_json, sender_member_id - FROM agent_inbox + FROM agent_org_runtime_inbox WHERE org_run_id=?1 AND recipient_member_id=?2 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id ) ORDER BY id ASC LIMIT ?3 @@ -215,23 +215,23 @@ pub(super) fn resolve_agent_org_wake_mode( THEN json_extract(control.payload_json, '$.mode') ELSE NULL END, EXISTS( - SELECT 1 FROM agent_org_tasks owned + SELECT 1 FROM agent_org_runtime_tasks owned WHERE owned.org_run_id=?1 AND owned.owner=?2 AND owned.status IN ('pending','in_progress') ) AS has_open_owned_task FROM control - LEFT JOIN agent_org_tasks assigned + LEFT JOIN agent_org_runtime_tasks assigned ON control.payload_kind='task_assigned' AND json_type(control.payload_json, '$.task_id')='text' AND assigned.org_run_id=?1 AND assigned.id=json_extract(control.payload_json, '$.task_id') - LEFT JOIN agent_org_plan_approvals approval + LEFT JOIN agent_org_runtime_plan_approvals approval ON control.payload_kind='plan_approval_response' AND json_type(control.payload_json, '$.request_id')='text' AND approval.org_run_id=?1 AND approval.request_id=json_extract(control.payload_json, '$.request_id') - LEFT JOIN agent_org_tasks approval_task + LEFT JOIN agent_org_runtime_tasks approval_task ON approval_task.org_run_id=?1 AND approval_task.id=approval.source_task_id ORDER BY control.id DESC", diff --git a/src-tauri/crates/agent-core/src/state/commands/session/message/tests.rs b/src-tauri/crates/agent-core/src/state/commands/session/message/tests.rs index 3f6b46b1a4..8549879557 100644 --- a/src-tauri/crates/agent-core/src/state/commands/session/message/tests.rs +++ b/src-tauri/crates/agent-core/src/state/commands/session/message/tests.rs @@ -258,7 +258,7 @@ fn queued_agent_org_wake_rechecks_run_member_and_intervention_at_turn_start() { ) .expect("restore member idle"); conn.execute( - "UPDATE agent_org_runs SET status='paused' WHERE id=?1", + "UPDATE agent_org_runtime_runs SET status='paused' WHERE id=?1", rusqlite::params![&fixture.run_id], ) .expect("pause run"); @@ -269,7 +269,7 @@ fn queued_agent_org_wake_rechecks_run_member_and_intervention_at_turn_start() { ); conn.execute( - "UPDATE agent_org_runs SET status='running' WHERE id=?1", + "UPDATE agent_org_runtime_runs SET status='running' WHERE id=?1", rusqlite::params![&fixture.run_id], ) .expect("resume run"); @@ -301,7 +301,7 @@ fn direct_agent_org_turn_only_promotes_while_run_is_running() { AgentOrgRunStatus::Archived, ] { conn.execute( - "UPDATE agent_org_runs SET status=?1 WHERE id=?2", + "UPDATE agent_org_runtime_runs SET status=?1 WHERE id=?2", rusqlite::params![status.as_str(), &fixture.run_id], ) .expect("set non-runnable run status"); @@ -326,7 +326,7 @@ fn direct_agent_org_turn_only_promotes_while_run_is_running() { } conn.execute( - "UPDATE agent_org_runs SET status=?1 WHERE id=?2", + "UPDATE agent_org_runtime_runs SET status=?1 WHERE id=?2", rusqlite::params![AgentOrgRunStatus::Running.as_str(), &fixture.run_id], ) .expect("restore running run"); diff --git a/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/group_chat.rs b/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/group_chat.rs index 20148a776b..da7a188ca0 100644 --- a/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/group_chat.rs +++ b/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/group_chat.rs @@ -136,8 +136,8 @@ pub(super) fn load_group_chat_history_page( substr(inbox.created_at, 1, 64), CASE WHEN inbox.read_at IS NULL THEN NULL ELSE substr(inbox.read_at, 1, 64) END, resolution.resolution_kind - FROM agent_inbox inbox - LEFT JOIN agent_inbox_delivery_resolutions resolution + FROM agent_org_runtime_inbox inbox + LEFT JOIN agent_org_runtime_inbox_delivery_resolutions resolution ON resolution.inbox_id=inbox.id WHERE inbox.org_run_id=?1 AND inbox.sender_agent_id=?2 @@ -425,7 +425,7 @@ pub(super) fn persist_group_chat_message( .map_err(|err| err.to_string())?; let run_status: Option = tx .query_row( - "SELECT status FROM agent_org_runs WHERE id=?1", + "SELECT status FROM agent_org_runtime_runs WHERE id=?1", params![&context.run_id], |row| row.get(0), ) @@ -469,13 +469,13 @@ pub(super) fn persist_group_chat_message( )?; if let Some(display_text) = display_text { tx.execute( - "UPDATE agent_inbox SET display_text=?1 WHERE id=?2", + "UPDATE agent_org_runtime_inbox SET display_text=?1 WHERE id=?2", params![display_text, row.id], ) .map_err(|err| err.to_string())?; } tx.execute( - "UPDATE agent_member_interventions + "UPDATE agent_org_runtime_member_interventions SET cleared_at=?3 WHERE org_run_id=?1 AND member_id=?2 AND cleared_at IS NULL", params![ diff --git a/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/lifecycle.rs b/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/lifecycle.rs index 711106f764..4dc7485e95 100644 --- a/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/lifecycle.rs +++ b/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/lifecycle.rs @@ -189,7 +189,7 @@ pub(super) fn resume_agent_org_context_sync( .map_err(|err| err.to_string())?; let status: Option = tx .query_row( - "SELECT status FROM agent_org_runs WHERE id=?1", + "SELECT status FROM agent_org_runtime_runs WHERE id=?1", params![&context.run_id], |row| row.get(0), ) @@ -199,7 +199,7 @@ pub(super) fn resume_agent_org_context_sync( let run_is_running = transitioned || status.as_deref() == Some("running"); if transitioned { tx.execute( - "UPDATE agent_org_runs + "UPDATE agent_org_runtime_runs SET status='running', updated_at=?2 WHERE id=?1 AND status='paused'", params![&context.run_id, chrono::Utc::now().to_rfc3339()], @@ -321,13 +321,13 @@ fn seed_coordinator_resume_inbox_in_tx( let has_unread: bool = tx .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_inbox + SELECT 1 FROM agent_org_runtime_inbox WHERE recipient_member_id=?1 AND org_run_id=?2 AND read_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM agent_inbox_delivery_resolutions resolution - WHERE resolution.inbox_id=agent_inbox.id + SELECT 1 FROM agent_org_runtime_inbox_delivery_resolutions resolution + WHERE resolution.inbox_id=agent_org_runtime_inbox.id ) )", params![coordinator_member_id, &context.run_id], diff --git a/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/run_view.rs b/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/run_view.rs index a899932033..0f463a531c 100644 --- a/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/run_view.rs +++ b/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/run_view.rs @@ -457,7 +457,7 @@ fn task_counts_by_owner_with_connection( COALESCE(SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status='in_progress' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END), 0) - FROM agent_org_tasks + FROM agent_org_runtime_tasks WHERE org_run_id=?1 AND owner IS NOT NULL GROUP BY owner", ) diff --git a/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/tests.rs b/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/tests.rs index d3ae7a4f8c..8c9939f5ae 100644 --- a/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/tests.rs +++ b/src-tauri/crates/agent-core/src/state/commands/session/org_tasks/tests.rs @@ -61,7 +61,7 @@ fn prepare_command_run(status: &str) -> AgentOrgRunContext { .expect("intervention schema"); let now = chrono::Utc::now().to_rfc3339(); conn.execute( - "INSERT INTO agent_org_runs ( + "INSERT INTO agent_org_runtime_runs ( id, org_id, coordinator_agent_id, root_session_id, org_snapshot_json, entry_mode, status, work_item_id, project_slug, routine_fire_id, summary, last_error, @@ -85,7 +85,7 @@ fn inbox_count_for_member(context: &AgentOrgRunContext, member_id: &str) -> usiz let conn = get_connection().expect("db connection"); let count: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_inbox + "SELECT COUNT(*) FROM agent_org_runtime_inbox WHERE org_run_id=?1 AND recipient_member_id=?2", params![&context.run_id, member_id], |row| row.get(0), @@ -252,7 +252,7 @@ fn run_view_is_a_pure_read_and_does_not_advance_updated_at() { .expect("read data version"); let before_updated_at: String = observer .query_row( - "SELECT updated_at FROM agent_org_runs WHERE id=?1", + "SELECT updated_at FROM agent_org_runtime_runs WHERE id=?1", [&context.run_id], |row| row.get(0), ) @@ -266,7 +266,7 @@ fn run_view_is_a_pure_read_and_does_not_advance_updated_at() { .expect("read data version after Run View"); let after_updated_at: String = observer .query_row( - "SELECT updated_at FROM agent_org_runs WHERE id=?1", + "SELECT updated_at FROM agent_org_runtime_runs WHERE id=?1", [&context.run_id], |row| row.get(0), ) @@ -448,7 +448,7 @@ fn group_message_and_intervention_clear_commit_atomically() { let conn = get_connection().expect("db connection"); conn.execute_batch( "CREATE TRIGGER reject_intervention_clear - BEFORE UPDATE OF cleared_at ON agent_member_interventions + BEFORE UPDATE OF cleared_at ON agent_org_runtime_member_interventions BEGIN SELECT RAISE(ABORT, 'injected intervention clear failure'); END;", @@ -535,7 +535,7 @@ fn group_chat_history_pages_all_rows_and_preserves_long_display_text_after_reloa let conn = get_connection().expect("db connection"); conn.execute( - "UPDATE agent_org_runs SET status='archived' WHERE id=?1", + "UPDATE agent_org_runtime_runs SET status='archived' WHERE id=?1", params![&context.run_id], ) .expect("archive run"); @@ -555,7 +555,7 @@ fn paused_resume_and_coordinator_seed_commit_or_rollback_together() { let conn = get_connection().expect("db connection"); conn.execute_batch( "CREATE TRIGGER reject_resume_seed - BEFORE INSERT ON agent_inbox + BEFORE INSERT ON agent_org_runtime_inbox BEGIN SELECT RAISE(ABORT, 'injected resume seed failure'); END;", @@ -569,7 +569,7 @@ fn paused_resume_and_coordinator_seed_commit_or_rollback_together() { let conn = get_connection().expect("db connection"); let status: String = conn .query_row( - "SELECT status FROM agent_org_runs WHERE id=?1", + "SELECT status FROM agent_org_runtime_runs WHERE id=?1", params![&context.run_id], |row| row.get(0), ) @@ -591,7 +591,7 @@ fn paused_resume_and_coordinator_seed_commit_or_rollback_together() { let conn = get_connection().expect("db connection"); let status: String = conn .query_row( - "SELECT status FROM agent_org_runs WHERE id=?1", + "SELECT status FROM agent_org_runtime_runs WHERE id=?1", params![&context.run_id], |row| row.get(0), ) @@ -702,7 +702,7 @@ fn return_to_work_rolls_back_intervention_clear_when_boundary_capture_fails() { }) .expect("enter intervention"); let conn = get_connection().expect("db connection"); - conn.execute("DROP TABLE agent_inbox", []) + conn.execute("DROP TABLE agent_org_runtime_inbox", []) .expect("inject boundary query failure"); drop(conn); @@ -711,7 +711,7 @@ fn return_to_work_rolls_back_intervention_clear_when_boundary_capture_fails() { "member-planner", ) .expect_err("boundary failure must abort return-to-work transaction"); - assert!(error.contains("agent_inbox")); + assert!(error.contains("agent_org_runtime_inbox")); assert!( AgentMemberInterventionStore::active_for_member(&context.run_id, "member-planner") .expect("load intervention after rollback") diff --git a/src-tauri/crates/agent-core/src/state/commands/session/persistence.rs b/src-tauri/crates/agent-core/src/state/commands/session/persistence.rs index 42d78631da..6bcd51d801 100644 --- a/src-tauri/crates/agent-core/src/state/commands/session/persistence.rs +++ b/src-tauri/crates/agent-core/src/state/commands/session/persistence.rs @@ -136,7 +136,7 @@ fn load_agent_org_session_delete_plan( let mut stmt = conn .prepare( "SELECT id, status - FROM agent_org_runs + FROM agent_org_runtime_runs WHERE root_session_id=?1 ORDER BY id", ) @@ -199,7 +199,7 @@ fn load_agent_org_session_delete_plan( descendant.cycle, ( SELECT nested.id - FROM agent_org_runs nested + FROM agent_org_runtime_runs nested WHERE nested.id<>?2 AND nested.root_session_id=descendant.session_id ORDER BY nested.id @@ -537,7 +537,7 @@ fn ensure_agent_org_hierarchy_absent( } let run_exists = conn .query_row( - "SELECT EXISTS(SELECT 1 FROM agent_org_runs WHERE id=?1)", + "SELECT EXISTS(SELECT 1 FROM agent_org_runtime_runs WHERE id=?1)", [&plan.run_id], |row| row.get::<_, bool>(0), ) @@ -967,7 +967,7 @@ mod tests { fn seed_run_with_status(run_id: &str, root_session_id: &str, status: &str) { let conn = get_connection().expect("sandbox DB"); conn.execute( - "INSERT INTO agent_org_runs ( + "INSERT INTO agent_org_runtime_runs ( id, org_id, coordinator_agent_id, root_session_id, entry_mode, status, created_at, updated_at ) VALUES (?1, 'org-delete-test', 'coordinator-agent', ?2, @@ -1016,7 +1016,7 @@ mod tests { fn seed_run_owned_rows(run_id: &str) { let conn = get_connection().expect("sandbox DB"); conn.execute( - "INSERT INTO agent_inbox ( + "INSERT INTO agent_org_runtime_inbox ( recipient_agent_id, recipient_member_id, sender_agent_id, org_run_id, payload_kind, payload_json, created_at ) VALUES ('worker-agent', 'worker', 'system', ?1, @@ -1025,7 +1025,7 @@ mod tests { ) .expect("seed run inbox history"); conn.execute( - "INSERT INTO agent_org_tasks ( + "INSERT INTO agent_org_runtime_tasks ( id, org_run_id, subject, status, created_at, updated_at ) VALUES (?1, ?2, 'delete me', 'completed', ?3, ?3)", rusqlite::params![format!("task-{run_id}"), run_id, "2026-07-16T00:00:00Z"], @@ -1092,26 +1092,30 @@ mod tests { ); } } - assert!(!row_exists("agent_org_runs", "id", "hierarchy-delete-run")); assert!(!row_exists( - "agent_inbox", + "agent_org_runtime_runs", + "id", + "hierarchy-delete-run" + )); + assert!(!row_exists( + "agent_org_runtime_inbox", "org_run_id", "hierarchy-delete-run" )); assert!(!row_exists( - "agent_org_tasks", + "agent_org_runtime_tasks", "org_run_id", "hierarchy-delete-run" )); assert!(row_exists("agent_sessions", "session_id", unrelated)); assert!(row_exists("agent_messages", "session_id", unrelated)); assert!(row_exists( - "agent_org_runs", + "agent_org_runtime_runs", "id", "hierarchy-delete-other-run" )); assert!(row_exists( - "agent_inbox", + "agent_org_runtime_inbox", "org_run_id", "hierarchy-delete-other-run" )); @@ -1140,9 +1144,13 @@ mod tests { assert!(!row_exists("agent_sessions", "session_id", worker)); assert!(row_exists("agent_sessions", "session_id", root)); - assert!(row_exists("agent_org_runs", "id", "hierarchy-worker-run")); assert!(row_exists( - "agent_inbox", + "agent_org_runtime_runs", + "id", + "hierarchy-worker-run" + )); + assert!(row_exists( + "agent_org_runtime_inbox", "org_run_id", "hierarchy-worker-run" )); @@ -1224,7 +1232,7 @@ mod tests { get_connection() .expect("sandbox DB") .query_row( - "SELECT status FROM agent_org_runs WHERE id='hierarchy-active-run'", + "SELECT status FROM agent_org_runtime_runs WHERE id='hierarchy-active-run'", [], |row| row.get::<_, String>(0) ) @@ -1233,7 +1241,11 @@ mod tests { ); assert!(row_exists("agent_sessions", "session_id", root)); assert!(row_exists("agent_sessions", "session_id", worker)); - assert!(row_exists("agent_org_runs", "id", "hierarchy-active-run")); + assert!(row_exists( + "agent_org_runtime_runs", + "id", + "hierarchy-active-run" + )); } #[test] @@ -1274,7 +1286,11 @@ mod tests { assert!(error.contains("shell replay calls are active")); assert!(row_exists("agent_sessions", "session_id", root)); assert!(row_exists("agent_sessions", "session_id", worker)); - assert!(row_exists("agent_org_runs", "id", "hierarchy-replay-run")); + assert!(row_exists( + "agent_org_runtime_runs", + "id", + "hierarchy-replay-run" + )); writer .finalize(core_types::session_event::ShellReplayStatus::Complete, None) @@ -1302,7 +1318,11 @@ mod tests { assert!(error.contains("repository path no longer exists")); assert!(row_exists("agent_sessions", "session_id", root)); assert!(row_exists("agent_sessions", "session_id", worker)); - assert!(row_exists("agent_org_runs", "id", "hierarchy-replay-run")); + assert!(row_exists( + "agent_org_runtime_runs", + "id", + "hierarchy-replay-run" + )); std::fs::remove_dir_all(replay_root).expect("remove replay fixture"); } @@ -1329,12 +1349,12 @@ mod tests { assert!(row_exists("agent_sessions", "session_id", session_id)); } assert!(row_exists( - "agent_org_runs", + "agent_org_runtime_runs", "id", "hierarchy-nested-outer-run" )); assert!(row_exists( - "agent_org_runs", + "agent_org_runtime_runs", "id", "hierarchy-nested-inner-run" )); @@ -1380,7 +1400,11 @@ mod tests { .expect_err("oversized hierarchy must fail closed"); assert!(error.contains("exceeds")); assert!(row_exists("agent_sessions", "session_id", limit_root)); - assert!(row_exists("agent_org_runs", "id", "hierarchy-limit-run")); + assert!(row_exists( + "agent_org_runtime_runs", + "id", + "hierarchy-limit-run" + )); } #[test] @@ -1406,7 +1430,11 @@ mod tests { for session_id in [root, worker, "hierarchy-recheck-late-worker"] { assert!(row_exists("agent_sessions", "session_id", session_id)); } - assert!(row_exists("agent_org_runs", "id", "hierarchy-recheck-run")); + assert!(row_exists( + "agent_org_runtime_runs", + "id", + "hierarchy-recheck-run" + )); } #[test] @@ -1454,14 +1482,18 @@ mod tests { ); } } - assert!(row_exists("agent_org_runs", "id", "hierarchy-rollback-run")); assert!(row_exists( - "agent_inbox", + "agent_org_runtime_runs", + "id", + "hierarchy-rollback-run" + )); + assert!(row_exists( + "agent_org_runtime_inbox", "org_run_id", "hierarchy-rollback-run" )); assert!(row_exists( - "agent_org_tasks", + "agent_org_runtime_tasks", "org_run_id", "hierarchy-rollback-run" )); @@ -1514,7 +1546,7 @@ mod tests { assert!(row_exists("agent_sessions", "session_id", worker)); assert!(!row_exists("agent_sessions", "session_id", injected)); assert!(row_exists( - "agent_org_runs", + "agent_org_runtime_runs", "id", "hierarchy-trigger-change-run" )); diff --git a/src-tauri/crates/app-paths/src/lib.rs b/src-tauri/crates/app-paths/src/lib.rs index 49b7ed8bdf..968de52d05 100644 --- a/src-tauri/crates/app-paths/src/lib.rs +++ b/src-tauri/crates/app-paths/src/lib.rs @@ -424,11 +424,18 @@ pub fn agent_definitions() -> PathBuf { orgii_root().join("agent-definitions.json") } -/// Agent organizations (global): `~/.orgii/agent-orgs.json`. +/// Retired Agent Org definitions path used only by pre-redesign builds: +/// `~/.orgii/agent-orgs.json`. pub fn agent_orgs() -> PathBuf { orgii_root().join("agent-orgs.json") } +/// Redesigned Agent Org definitions (global): +/// `~/.orgii/agent-org-definitions.json`. +pub fn agent_org_definitions() -> PathBuf { + orgii_root().join("agent-org-definitions.json") +} + /// Builtin-agent overrides overlay: `~/.orgii/builtin-overrides.json`. /// /// User-writable overlay for `builtin:*` agent definitions. Loaded on diff --git a/src-tauri/crates/e2e-test/src/agent_org.rs b/src-tauri/crates/e2e-test/src/agent_org.rs index 393ba32139..d8163848c5 100644 --- a/src-tauri/crates/e2e-test/src/agent_org.rs +++ b/src-tauri/crates/e2e-test/src/agent_org.rs @@ -2727,7 +2727,7 @@ async fn post_member_idle_with_failure_reason( /// agent_id and `reason = "available"`. /// 2. The production `InboxStoreMemberIdleHook` (installed at app /// boot, no test override) persists a `MemberIdle` envelope into -/// `agent_inbox` addressed from `_system` to the coordinator. +/// `agent_org_runtime_inbox` addressed from `_system` to the coordinator. /// 3. A subsequent `inbox/list-by-run` asserts the row exists with /// the right sender / recipient / payload shape — i.e. the LLM /// cannot forge it from a peer, and the coordinator's next drain diff --git a/src-tauri/crates/session-persistence/src/crud.rs b/src-tauri/crates/session-persistence/src/crud.rs index fbf0910ca6..f0c0964584 100644 --- a/src-tauri/crates/session-persistence/src/crud.rs +++ b/src-tauri/crates/session-persistence/src/crud.rs @@ -611,7 +611,7 @@ pub fn clear_old_sessions(max_age_hours: i64) -> SqliteResult { let _ = tx.execute("DELETE FROM agent_snapshots WHERE session_id = ?1", [sid]); let _ = tx.execute("DELETE FROM goal_loop_state WHERE session_id = ?1", [sid]); let _ = tx.execute( - "DELETE FROM agent_member_interventions WHERE session_id = ?1", + "DELETE FROM agent_org_runtime_member_interventions WHERE session_id = ?1", [sid], ); } diff --git a/src-tauri/crates/session-persistence/src/turn_intents.rs b/src-tauri/crates/session-persistence/src/turn_intents.rs index b4e1bf3eb0..6339fdbc44 100644 --- a/src-tauri/crates/session-persistence/src/turn_intents.rs +++ b/src-tauri/crates/session-persistence/src/turn_intents.rs @@ -462,7 +462,7 @@ pub fn reconcile_agent_org_in_flight_after_restart( AND NOT ( status = 'queued' AND EXISTS ( - SELECT 1 FROM agent_org_initial_inputs initial + SELECT 1 FROM agent_org_runtime_initial_inputs initial WHERE initial.org_run_id=session_turn_intents.org_run_id AND initial.turn_intent_id=session_turn_intents.turn_intent_id AND initial.status IN ('queued', 'dispatched') @@ -911,7 +911,7 @@ mod tests { .expect("init Agent Org schemas"); let now = Utc::now().to_rfc3339(); conn.execute( - "INSERT INTO agent_org_runs ( + "INSERT INTO agent_org_runtime_runs ( id, org_id, coordinator_agent_id, root_session_id, entry_mode, status, has_initial_work, created_at, updated_at ) VALUES (?1, 'restart-org', 'coordinator', ?2, @@ -937,7 +937,7 @@ mod tests { .expect("seed Agent Org intent"); } conn.execute( - "INSERT INTO agent_org_initial_inputs ( + "INSERT INTO agent_org_runtime_initial_inputs ( org_run_id, turn_intent_id, message_id, content, payload_json, status, created_at, updated_at ) VALUES (?1, 'queued-canonical-initial', 'initial-message', diff --git a/src-tauri/crates/types/src/tool_names.rs b/src-tauri/crates/types/src/tool_names.rs index 5e58bd0c21..6a8d64a901 100644 --- a/src-tauri/crates/types/src/tool_names.rs +++ b/src-tauri/crates/types/src/tool_names.rs @@ -104,7 +104,7 @@ pub const MANAGE_AGENT_DEF: &str = "manage_agent_def"; /// Typed messaging inside an Agent Org run. Distinct from [`SEND_MESSAGE`] /// (chat-channel egress) — this targets coordinator/member participants in /// the same org by name or stable agent_id and persists to the typed -/// `agent_inbox` table. +/// `agent_org_runtime_inbox` table. pub const ORG_SEND_MESSAGE: &str = "org_send_message"; // ── Agent Org Tasks ───────────────────────────────────────────────── diff --git a/src-tauri/src/agent_sessions/session_directory/aggregation.rs b/src-tauri/src/agent_sessions/session_directory/aggregation.rs index 4ee5386def..a8d7e975f3 100644 --- a/src-tauri/src/agent_sessions/session_directory/aggregation.rs +++ b/src-tauri/src/agent_sessions/session_directory/aggregation.rs @@ -1692,7 +1692,7 @@ mod tests { .expect("seed coding session"); } conn.execute( - "INSERT INTO agent_org_runs ( + "INSERT INTO agent_org_runtime_runs ( id, org_id, coordinator_agent_id, root_session_id, entry_mode, status, created_at, updated_at ) VALUES ( diff --git a/src-tauri/src/api/agent/test/agent_org.rs b/src-tauri/src/api/agent/test/agent_org.rs index 4100c2b439..40f6ade857 100644 --- a/src-tauri/src/api/agent/test/agent_org.rs +++ b/src-tauri/src/api/agent/test/agent_org.rs @@ -1541,7 +1541,7 @@ pub async fn test_agent_org_durable_invariants( let conn = database::db::get_connection().map_err(|err| err.to_string())?; let run_row: Option<(String, Option)> = conn .query_row( - "SELECT status, root_session_id FROM agent_org_runs WHERE id = ?1", + "SELECT status, root_session_id FROM agent_org_runtime_runs WHERE id = ?1", params![org_run_id], |row| Ok((row.get(0)?, row.get(1)?)), ) @@ -1553,7 +1553,7 @@ pub async fn test_agent_org_durable_invariants( let open_task_count: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_org_tasks + "SELECT COUNT(*) FROM agent_org_runtime_tasks WHERE org_run_id = ?1 AND status IN ('pending', 'in_progress')", params![org_run_id], |row| row.get(0), @@ -1561,7 +1561,7 @@ pub async fn test_agent_org_durable_invariants( .map_err(|err| err.to_string())?; let ownerless_in_progress_count: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_org_tasks + "SELECT COUNT(*) FROM agent_org_runtime_tasks WHERE org_run_id = ?1 AND status = 'in_progress' AND (owner IS NULL OR TRIM(owner) = '')", @@ -1571,7 +1571,7 @@ pub async fn test_agent_org_durable_invariants( .map_err(|err| err.to_string())?; let unread_inbox_count: i64 = conn .query_row( - "SELECT COUNT(*) FROM agent_inbox + "SELECT COUNT(*) FROM agent_org_runtime_inbox WHERE org_run_id = ?1 AND read_at IS NULL", params![org_run_id], |row| row.get(0), @@ -1713,12 +1713,7 @@ pub async fn test_agent_org_seed_stale_worker_run( agent_core::foundation::persistence::session_snapshots::ensure_tables_with(&conn) .map_err(|err| err.to_string())?; agent_core::core::session::persistence::init(&conn).map_err(|err| err.to_string())?; - agent_core::coordination::agent_org_runs::init_schema(&conn) - .map_err(|err| err.to_string())?; - agent_core::coordination::agent_member_interventions::init_schema(&conn) - .map_err(|err| err.to_string())?; - agent_core::coordination::agent_org_tasks::init_schema(&conn) - .map_err(|err| err.to_string())?; + agent_core::coordination::init_agent_org_schemas(&conn).map_err(|err| err.to_string())?; let now = fixture_updated_at.unwrap_or_else(|| chrono::Utc::now().to_rfc3339()); upsert_session(&UnifiedSessionRecord { @@ -1931,7 +1926,7 @@ pub async fn test_agent_org_session_delete_snapshot( for run_id in run_ids { let exists = conn .query_row( - "SELECT EXISTS(SELECT 1 FROM agent_org_runs WHERE id=?1)", + "SELECT EXISTS(SELECT 1 FROM agent_org_runtime_runs WHERE id=?1)", [&run_id], |row| row.get::<_, bool>(0), ) @@ -2629,7 +2624,7 @@ pub async fn test_agent_org_run_cleanup( let mut stmt = conn .prepare( "SELECT id - FROM agent_org_runs + FROM agent_org_runtime_runs WHERE org_id LIKE ?1 AND (?2 IS NULL OR id = ?2)", ) @@ -3061,12 +3056,7 @@ pub async fn test_agent_org_seed_cli_member_run( agent_core::foundation::persistence::session_snapshots::ensure_tables_with(&conn) .map_err(|err| err.to_string())?; agent_core::core::session::persistence::init(&conn).map_err(|err| err.to_string())?; - agent_core::coordination::agent_org_runs::init_schema(&conn) - .map_err(|err| err.to_string())?; - agent_core::coordination::agent_member_interventions::init_schema(&conn) - .map_err(|err| err.to_string())?; - agent_core::coordination::agent_org_tasks::init_schema(&conn) - .map_err(|err| err.to_string())?; + agent_core::coordination::init_agent_org_schemas(&conn).map_err(|err| err.to_string())?; crate::agent_sessions::cli::init_cli_agent_tables(&conn).map_err(|err| err.to_string())?; let now = chrono::Utc::now().to_rfc3339(); diff --git a/src-tauri/src/setup/hooks.rs b/src-tauri/src/setup/hooks.rs index a64424558a..ad387e457f 100644 --- a/src-tauri/src/setup/hooks.rs +++ b/src-tauri/src/setup/hooks.rs @@ -64,7 +64,13 @@ pub(crate) fn register_database_schemas() { ); } - agent_core::coordination::init_agent_org_schemas(conn)?; + // Scoped degradation: a failed Agent Org runtime namespace must not + // take sessions.db (and with it ordinary chat) down. The scoped + // variant logs the full diagnostic, records the failure in + // `coordination::availability` — where every Agent Org command/store + // entry consults it and returns a structured unavailable error — + // and lets the remaining sessions.db initializers proceed. + agent_core::coordination::init_agent_org_schemas_scoped(conn); match session_persistence::turn_intents::reconcile_agent_org_in_flight_after_restart(conn) { Ok(0) => {} Ok(count) => tracing::info!( diff --git a/src-tauri/src/test_utils/test_env.rs b/src-tauri/src/test_utils/test_env.rs index 37b9979014..5be4fd693b 100644 --- a/src-tauri/src/test_utils/test_env.rs +++ b/src-tauri/src/test_utils/test_env.rs @@ -98,23 +98,27 @@ mod tests { assert!(schema_object_exists( &conn, "table", - "agent_org_run_progress" + "agent_org_runtime_run_progress" )); assert!(column_exists( &conn, - "agent_org_run_progress", + "agent_org_runtime_run_progress", "work_revision" )); assert!(schema_object_exists( &conn, "table", - "agent_org_recovery_attempts" + "agent_org_runtime_recovery_attempts" + )); + assert!(column_exists( + &conn, + "agent_org_runtime_inbox", + "causation_inbox_id" )); - assert!(column_exists(&conn, "agent_inbox", "causation_inbox_id")); assert!(schema_object_exists( &conn, "index", - "idx_agent_inbox_causation_recipient_once" + "idx_agent_org_runtime_inbox_causation_recipient_once" )); } }