From b7e24bd1515b058672c2ba8959465214f1ecd968 Mon Sep 17 00:00:00 2001 From: Shibo Sheng Date: Fri, 14 Aug 2026 11:22:08 +0800 Subject: [PATCH 1/3] fix(agent-org): isolate runtime persistence namespace Move the redesigned Agent Org runtime into the canonical agent_org_runtime_* tables and retire the exact legacy table set atomically on startup. Isolate flat Team definitions at agent-org-definitions.json while preserving ordinary Rust and CLI session data across downgrade and re-upgrade. Pre-commit hook ran. Total eslint: 0, total circular: 0 --- .../core/coordination/agent_inbox/message.rs | 14 +- .../src/core/coordination/agent_inbox/mod.rs | 1 + .../core/coordination/agent_inbox/schema.rs | 129 ++-- .../coordination/agent_inbox/store_drain.rs | 64 +- .../coordination/agent_inbox/store_read.rs | 54 +- .../coordination/agent_inbox/store_write.rs | 8 +- .../core/coordination/agent_inbox/tests.rs | 22 +- .../agent_member_interventions.rs | 42 +- .../agent_org_plan_approvals/artifact.rs | 2 +- .../agent_org_plan_approvals/mod.rs | 14 +- .../agent_org_plan_approvals/persistence.rs | 4 +- .../agent_org_plan_approvals/store.rs | 26 +- .../agent_org_plan_approvals/tests.rs | 12 +- .../agent_org_plan_approvals/transitions.rs | 10 +- .../coordination/agent_org_runs/helpers.rs | 6 +- .../agent_org_runs/materialization.rs | 30 +- .../core/coordination/agent_org_runs/mod.rs | 97 +-- .../coordination/agent_org_runs/progress.rs | 30 +- .../coordination/agent_org_runs/quiescence.rs | 26 +- .../core/coordination/agent_org_runs/store.rs | 91 +-- .../core/coordination/agent_org_runs/tests.rs | 295 +------- .../coordination/agent_org_tasks/helpers.rs | 4 +- .../core/coordination/agent_org_tasks/mod.rs | 77 +- .../agent_org_tasks/store/create.rs | 4 +- .../agent_org_tasks/store/delete.rs | 4 +- .../agent_org_tasks/store/dependencies.rs | 23 +- .../agent_org_tasks/store/read.rs | 14 +- .../agent_org_tasks/store/requeue.rs | 26 +- .../agent_org_tasks/store/update.rs | 6 +- .../agent_org_tasks/store/validation.rs | 4 +- .../coordination/agent_org_tasks/tests.rs | 39 +- .../core/coordination/agent_org_watchdog.rs | 1 + .../coordination/agent_org_watchdog/budget.rs | 18 +- .../agent_org_watchdog/inspect.rs | 6 +- .../agent_org_watchdog/recover.rs | 22 +- .../agent_org_watchdog/reservation.rs | 12 +- .../coordination/agent_org_watchdog/tests.rs | 6 +- .../agent-core/src/core/coordination/mod.rs | 10 +- .../src/core/coordination/schema.rs | 692 ++++++++++++++++++ .../agent-core/src/core/definitions/orgs.rs | 228 +++--- .../src/core/session/persistence/messages.rs | 8 +- .../src/core/session/persistence/sidebar.rs | 10 +- .../turn/processor/inbox_drain/tests.rs | 4 +- .../orchestration/agent_org/inbox_repair.rs | 4 +- .../agent_org/send_message/persistence.rs | 2 +- .../agent_org/send_message/tests.rs | 4 +- .../orchestration/agent_org/task_tests.rs | 18 +- .../impls/orchestration/context_builders.rs | 28 +- .../tools/impls/orchestration/member_idle.rs | 2 +- .../db_helpers/messages/cleanup.rs | 8 +- .../foundation/persistence/db_helpers/mod.rs | 4 +- .../tool_infra/project/execution.rs | 61 +- src-tauri/crates/agent-core/src/lifecycle.rs | 11 +- .../commands/session/message/org_wake.rs | 22 +- .../state/commands/session/message/tests.rs | 8 +- .../commands/session/org_tasks/group_chat.rs | 10 +- .../commands/session/org_tasks/lifecycle.rs | 10 +- .../commands/session/org_tasks/run_view.rs | 2 +- .../state/commands/session/org_tasks/tests.rs | 22 +- .../src/state/commands/session/persistence.rs | 82 ++- src-tauri/crates/app-paths/src/lib.rs | 9 +- .../crates/session-persistence/src/crud.rs | 2 +- .../session-persistence/src/turn_intents.rs | 6 +- .../session_directory/aggregation.rs | 2 +- src-tauri/src/api/agent/test/agent_org.rs | 26 +- src-tauri/src/test_utils/test_env.rs | 14 +- 66 files changed, 1419 insertions(+), 1133 deletions(-) create mode 100644 src-tauri/crates/agent-core/src/core/coordination/schema.rs 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..c56bb6f9b2 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 @@ -12,18 +12,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 +46,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 +83,13 @@ 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( + DROP INDEX IF EXISTS idx_agent_org_runtime_inbox_run_task_assignment_v3; + DROP INDEX IF EXISTS idx_agent_org_runtime_inbox_run_task_assignment_v2; + CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_request_id + ON agent_org_runtime_inbox(request_id); + DROP INDEX IF EXISTS idx_agent_org_runtime_inbox_causation_once; + 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 +98,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 +112,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 +137,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 +162,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 +176,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..1f0f1e80bc 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 @@ -26,13 +26,13 @@ impl AgentInboxStore { let conn = get_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], @@ -60,13 +60,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", ) @@ -90,13 +90,13 @@ impl AgentInboxStore { ) -> Result, String> { let conn = get_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), @@ -115,14 +115,14 @@ impl AgentInboxStore { let conn = get_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), @@ -165,13 +165,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", @@ -255,16 +255,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 +282,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 +299,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 +319,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 +354,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 +364,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..455301f0e6 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 @@ -25,12 +25,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 +39,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 +121,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], @@ -185,7 +185,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", ) @@ -245,7 +245,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", @@ -303,7 +303,7 @@ impl AgentInboxStore { let conn = get_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), ) @@ -357,7 +357,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![ @@ -465,7 +465,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 +531,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 +570,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 +587,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 +608,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()))?; @@ -677,7 +677,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 @@ -788,11 +788,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 @@ -841,11 +841,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", @@ -906,7 +906,7 @@ impl AgentInboxStore { 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 +940,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 @@ -1018,13 +1018,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..161c4ce0ad 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 @@ -46,7 +46,7 @@ impl AgentInboxStore { 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), @@ -146,7 +146,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 +160,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 +209,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..5bb03a348a 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 { @@ -403,7 +403,7 @@ fn open_assignment_snapshot_uses_current_tasks_and_expression_index() { 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 +449,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!( @@ -469,7 +469,7 @@ fn assignment_snapshot_requires_current_owner_and_valid_typed_payload() { 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 +500,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 +515,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 +830,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..4b1023b174 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 @@ -57,8 +57,12 @@ pub struct AgentMemberInterventionRecord { } 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 +75,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);", ) } @@ -113,7 +117,7 @@ impl AgentMemberInterventionStore { with_sessions_writer(|| -> Result<(), String> { let conn = get_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 +155,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 ) })?; @@ -165,7 +169,7 @@ impl AgentMemberInterventionStore { let conn = get_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], @@ -197,7 +201,7 @@ impl AgentMemberInterventionStore { .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 +209,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), @@ -238,7 +242,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, @@ -276,7 +280,7 @@ impl AgentMemberInterventionStore { with_sessions_writer(|| -> Result { let conn = get_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 ( @@ -303,7 +307,7 @@ impl AgentMemberInterventionStore { let conn = get_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], @@ -335,7 +339,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 @@ -395,7 +399,7 @@ mod tests { let sandbox = test_helpers::test_env::sandbox(); let conn = get_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 } @@ -455,7 +459,7 @@ mod tests { let now = chrono::Utc::now(); let conn = get_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)", @@ -568,7 +572,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..041ca5e152 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 @@ -431,7 +431,7 @@ pub(super) fn list_distinct_plan_paths_after( 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", 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..f48e937114 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 @@ -149,8 +149,12 @@ pub struct AgentOrgPlanInboxDelivery { } 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 +173,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..6a5d8f2c62 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 @@ -227,7 +227,7 @@ impl AgentOrgPlanApprovalStore { 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", ) @@ -247,7 +247,7 @@ impl AgentOrgPlanApprovalStore { let conn = get_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", ) @@ -280,7 +280,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", ) @@ -449,7 +449,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 +463,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![ @@ -630,15 +630,15 @@ impl AgentOrgPlanApprovalStore { 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 +656,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..2d4548168a 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 @@ -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], @@ -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..75f897619e 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,16 @@ impl AgentOrgStartingFailure { } } -/// Initialize runtime Agent Org tables in `sessions.db`. +/// Initialize the redesigned runtime run envelope in an already-isolated +/// namespace. Production startup uses the complete schema coordinator; this +/// narrower entry point remains available to focused unit tests. 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 +423,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 54f1f52fe8..fc99249a46 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 @@ -107,7 +107,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" ); @@ -349,8 +349,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 +402,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 @@ -435,7 +435,7 @@ impl AgentOrgRunStore { 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 +465,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 +509,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 +559,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 +579,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' @@ -605,7 +605,7 @@ impl AgentOrgRunStore { let connection = get_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')", @@ -630,7 +630,7 @@ impl AgentOrgRunStore { let conn = get_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 @@ -678,7 +678,7 @@ impl AgentOrgRunStore { let conn = get_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 @@ -709,7 +709,7 @@ impl AgentOrgRunStore { .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, @@ -806,7 +806,7 @@ impl AgentOrgRunStore { .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 +824,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", ) @@ -918,7 +918,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 +927,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![ @@ -996,7 +996,7 @@ impl AgentOrgRunStore { let conn = get_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 +1018,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 +1029,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); @@ -1073,7 +1073,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", @@ -1126,7 +1126,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 @@ -1155,7 +1155,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), ) @@ -1198,10 +1198,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 )", @@ -1225,25 +1225,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"), @@ -1252,7 +1252,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 { @@ -1320,7 +1323,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", @@ -1394,7 +1397,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), ) @@ -1432,7 +1435,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), ) @@ -1455,7 +1458,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 ) @@ -1464,7 +1467,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 db0b21efb9..5289da03da 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 @@ -794,7 +539,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, @@ -807,7 +552,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, @@ -820,7 +565,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)", @@ -828,7 +573,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], @@ -845,14 +590,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( @@ -1617,14 +1362,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', @@ -1653,7 +1398,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), @@ -1777,7 +1522,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..81f453ed9d 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 @@ -653,8 +653,13 @@ pub fn new_task_id() -> String { /// 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 +674,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 +690,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 @@ -822,7 +801,7 @@ pub(crate) fn enqueue_task_assignments_if_still_ready_for_recovery( 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 +851,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..02a14f83ea 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 @@ -146,7 +146,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 @@ -327,7 +327,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..6d1ccbfde9 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 @@ -80,7 +80,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 +94,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..0ddb5087c2 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)", @@ -64,23 +64,14 @@ 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 + FROM agent_org_runtime_tasks task WHERE NOT EXISTS ( - SELECT 1 FROM agent_org_task_run_schema_migrations migration + SELECT 1 FROM agent_org_runtime_task_schema_migrations migration WHERE migration.name=?1 AND migration.org_run_id=task.org_run_id ) @@ -105,7 +96,7 @@ pub(super) fn normalize_legacy_dependency_rows( let already_applied: bool = conn .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_task_run_schema_migrations + SELECT 1 FROM agent_org_runtime_task_schema_migrations WHERE name=?1 AND org_run_id=?2 )", params![MIGRATION_NAME, &run_id], @@ -127,7 +118,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()], @@ -159,7 +150,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 +174,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..6bc5753453 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 @@ -62,7 +62,7 @@ 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 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) @@ -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", @@ -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 @@ -559,7 +559,7 @@ impl AgentOrgTaskStore { .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..c6ec0dcf66 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 @@ -45,7 +45,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 +75,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 @@ -162,7 +162,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 +193,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![ @@ -243,7 +243,7 @@ mod migration_tests { [("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 +252,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 +269,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 +282,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), @@ -293,7 +293,7 @@ mod migration_tests { let valid_marked: bool = conn .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_task_run_schema_migrations + SELECT 1 FROM agent_org_runtime_task_schema_migrations WHERE name='canonical_blocked_by_v1' AND org_run_id='valid-run' )", [], @@ -303,7 +303,7 @@ mod migration_tests { let corrupt_marked: bool = conn .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_task_run_schema_migrations + SELECT 1 FROM agent_org_runtime_task_schema_migrations WHERE name='canonical_blocked_by_v1' AND org_run_id='corrupt-run' )", [], @@ -317,7 +317,7 @@ mod migration_tests { ); 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'", [], ) @@ -326,7 +326,7 @@ mod migration_tests { let corrupt_marked_after_retry: bool = conn .query_row( "SELECT EXISTS( - SELECT 1 FROM agent_org_task_run_schema_migrations + SELECT 1 FROM agent_org_runtime_task_schema_migrations WHERE name='canonical_blocked_by_v1' AND org_run_id='corrupt-run' )", [], 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..3a3ed18314 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 @@ -46,7 +46,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 +94,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![ @@ -329,7 +329,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 94ba20f85e..6a04889b04 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 @@ -36,6 +36,7 @@ mod reservation; mod tests; pub use budget::clear_rewake_budget; +pub(crate) use budget::create_schema; pub use budget::init_schema; pub(crate) use budget::member_rewake_fingerprint; #[cfg(test)] 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 b57782a6a4..51a3a3ea4d 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 @@ -7,8 +7,12 @@ use super::*; 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 +23,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);", ) } @@ -52,7 +56,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)?)), @@ -125,7 +129,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 +147,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 @@ -170,7 +174,7 @@ pub fn clear_rewake_budget(run_id: &str, member_id: &str) -> Result<(), String> with_sessions_writer(|| { let conn = get_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 d841e37bee..05764c15e5 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], 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 e935bf77e5..6558197553 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 @@ -221,7 +221,7 @@ fn clear_coordinator_notice_budget_if_recovered(run_id: &str) -> Result<(), Stri .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], ) @@ -261,7 +261,7 @@ fn insert_member_continuation_if_tasks_current( 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), @@ -286,11 +286,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], @@ -309,7 +309,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())?; @@ -395,7 +395,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'", @@ -413,7 +413,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), ) @@ -554,13 +554,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..50ef3b93f1 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 @@ -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", @@ -98,7 +98,7 @@ pub(crate) fn commit_member_rewake_reservation( with_sessions_writer(|| -> Result<(), String> { let conn = get_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", @@ -125,7 +125,7 @@ pub(crate) fn refund_member_rewake_reservation( 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 b5a2b1f3b2..61565a6e67 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 @@ -152,7 +152,7 @@ fn running_query_is_limited_and_never_visits_quiet_states() { 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', @@ -167,7 +167,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', @@ -214,7 +214,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/mod.rs b/src-tauri/crates/agent-core/src/core/coordination/mod.rs index b9b9e3dff8..ef86d4e571 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/mod.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/mod.rs @@ -33,15 +33,11 @@ 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) } 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..53cc797c52 --- /dev/null +++ b/src-tauri/crates/agent-core/src/core/coordination/schema.rs @@ -0,0 +1,692 @@ +//! 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: a partial +//! or structurally unknown runtime namespace fails closed before any legacy +//! object is dropped. + +use std::collections::BTreeMap; + +use rusqlite::{ffi, Connection, Error as SqliteError, Result as SqliteResult}; + +use super::{ + agent_inbox, agent_member_interventions, agent_org_plan_approvals, agent_org_runs, + agent_org_tasks, agent_org_watchdog, +}; + +const RUNTIME_TABLES: [&str; 13] = [ + "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') + 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;"; + +type SchemaManifest = BTreeMap<(String, String), (String, String)>; + +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 fresh = match runtime_table_count { + 0 => true, + count if count == RUNTIME_TABLES.len() => { + verify_manifest(&tx, &expected)?; + false + } + count => { + return Err(schema_error(format!( + "partial Agent Org runtime schema: found {count} of {} canonical tables", + RUNTIME_TABLES.len() + ))) + } + }; + + let legacy_table_count = count_known_tables(&tx, &LEGACY_TABLES)?; + let legacy_object_count = count_legacy_objects(&tx)?; + tx.execute_batch(DROP_LEGACY_SCHEMA)?; + + if fresh { + create_runtime_schema(&tx)?; + } + verify_manifest(&tx, &expected)?; + 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, + idempotent = !fresh, + "initialized isolated Agent Org runtime schema" + ); + Ok(()) +} + +fn create_runtime_schema(conn: &Connection) -> SqliteResult<()> { + 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) +} + +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() +} + +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')", + )?; + 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') + 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, Instant}; + + 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}"); + assert_eq!(row_count(&conn, table), 0, "fresh {table} not empty"); + } + 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); + } + } + + #[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(), 13); + } + + #[test] + fn measures_constant_scale_startup_paths() { + const SAMPLES: usize = 25; + let mut fresh = Vec::with_capacity(SAMPLES); + let mut no_op = Vec::with_capacity(SAMPLES); + let mut cleanup = Vec::with_capacity(SAMPLES); + + for _ in 0..SAMPLES { + let conn = connection(); + let started = Instant::now(); + initialize(&conn).expect("fresh init"); + fresh.push(started.elapsed()); + + let started = Instant::now(); + initialize(&conn).expect("canonical no-op init"); + no_op.push(started.elapsed()); + + create_legacy_fixture(&conn, 13, true); + let started = Instant::now(); + initialize(&conn).expect("legacy cleanup init"); + cleanup.push(started.elapsed()); + } + + fn summary(samples: &mut [Duration]) -> (Duration, Duration) { + samples.sort_unstable(); + (samples[samples.len() / 2], *samples.last().unwrap()) + } + let (fresh_median, fresh_max) = summary(&mut fresh); + let (no_op_median, no_op_max) = summary(&mut no_op); + let (cleanup_median, cleanup_max) = summary(&mut cleanup); + eprintln!( + "Agent Org schema init, {SAMPLES} samples: fresh median={fresh_median:?} max={fresh_max:?}; canonical no-op median={no_op_median:?} max={no_op_max:?}; 13-table cleanup median={cleanup_median:?} max={cleanup_max:?}" + ); + } +} 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 eed40e4c01..2bb5841595 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))] @@ -364,7 +363,6 @@ struct AgentOrgDefinitionsFile { enum LoadOutcome { Missing, Loaded(Vec), - LegacyReset, Blocked(String), } @@ -381,11 +379,11 @@ impl Default for AgentOrgsStore { impl AgentOrgsStore { pub fn new() -> Self { + retire_legacy_definitions_file(); let path = storage_path(); let (mut orgs, persistence_blocked, should_persist) = match load_from_disk(&path) { LoadOutcome::Missing => (Vec::new(), None, true), LoadOutcome::Loaded(orgs) => (orgs, None, false), - LoadOutcome::LegacyReset => (Vec::new(), None, true), LoadOutcome::Blocked(message) => { error!("[agent-orgs] {}", message); (Vec::new(), Some(message), false) @@ -984,123 +982,64 @@ fn load_from_disk(path: &Path) -> LoadOutcome { return LoadOutcome::Blocked(format!("Failed to read {}: {}", path.display(), err)); } }; - let value: serde_json::Value = match serde_json::from_slice(&bytes) { - Ok(value) => value, - Err(err) => { - return LoadOutcome::Blocked(format!("Failed to parse {}: {}", path.display(), err)); - } + let definitions = match parse_definitions_content(&bytes, path) { + Ok(definitions) => definitions, + Err(err) => return LoadOutcome::Blocked(err), }; + info!( + "[agent-orgs] Loaded {} definitions from {}", + definitions.len(), + path.display() + ); + LoadOutcome::Loaded(definitions) +} - if 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 mut file: AgentOrgDefinitionsFile = match serde_json::from_value(value) { - Ok(file) => file, - Err(err) => { - return LoadOutcome::Blocked(format!( - "Unrecognized Agent Org definitions file {}: {}", - path.display(), - err - )); - } - }; +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 mut 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 LoadOutcome::Blocked(format!( + return Err(format!( "Unsupported Agent Org definitions schema version {} in {}", file.schema_version, path.display() )); } - if let Err(err) = canonicalize_and_validate_definitions(&mut file.definitions) { - return LoadOutcome::Blocked(format!( + canonicalize_and_validate_definitions(&mut file.definitions).map_err(|err| { + format!( "Invalid Agent Org definitions in {}: {}", path.display(), err - )); - } - info!( - "[agent-orgs] Loaded {} definitions from {}", - file.definitions.len(), - path.display() - ); - LoadOutcome::Loaded(file.definitions) + ) + })?; + Ok(file.definitions) } -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, - } -} - -fn backup_legacy_file(path: &Path, bytes: &[u8]) -> Result { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|err| { - format!( - "System clock error while backing up legacy definitions: {}", - err - ) - })? - .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); - } - 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 - )); - } - } +fn retire_legacy_definitions_file() { + let legacy_path = app_paths::agent_orgs(); + 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" + ), } - Err("Could not allocate a unique legacy Agent Org backup path".to_string()) } fn save_to_disk(path: &Path, orgs: &[OrgDefinition]) -> Result<(), String> { @@ -1310,21 +1249,63 @@ mod tests { } #[test] - fn legacy_array_is_backed_up_before_reset() { + fn legacy_live_file_is_deleted_without_parsing_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 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] @@ -1350,8 +1331,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 7cad86184d..66320a8394 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 0e8cbf1bf0..ab7ac25b11 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 c4a09dcbc7..e7eba50b39 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 6e13b40aad..e55621e8d2 100644 --- a/src-tauri/crates/agent-core/src/lifecycle.rs +++ b/src-tauri/crates/agent-core/src/lifecycle.rs @@ -827,15 +827,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 9c03bb1d2d..5812298034 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 @@ -127,8 +127,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 @@ -416,7 +416,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), ) @@ -460,13 +460,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 03c18c82ed..104cee25ce 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 @@ -451,7 +451,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 0ba9d68f9c..9b32c3ce5c 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 @@ -137,7 +137,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", ) @@ -200,7 +200,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 @@ -503,7 +503,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), ) @@ -933,7 +933,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, @@ -982,7 +982,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, @@ -991,7 +991,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"], @@ -1058,26 +1058,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" )); @@ -1106,9 +1110,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" )); @@ -1136,7 +1144,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) ) @@ -1145,7 +1153,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] @@ -1186,7 +1198,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) @@ -1214,7 +1230,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"); } @@ -1241,12 +1261,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" )); @@ -1292,7 +1312,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] @@ -1318,7 +1342,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] @@ -1366,14 +1394,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" )); @@ -1426,7 +1458,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/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/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/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" )); } } From 1dce106962915f36abf86cc1160a6255132653be Mon Sep 17 00:00:00 2001 From: Neonforge <48338160+Neonforge98@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:37:27 -0700 Subject: [PATCH 2/3] chore(agent-org): namespace hygiene sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - drop the three ghost DROP INDEX statements in the inbox DDL (…run_task_assignment_v2/_v3 and …causation_once): those names never existed in the runtime namespace, and the schema manifest owns versioning now. sqlite_master is unchanged, so the expected manifest regenerates identically. - include 'view' in the coordinator's three sqlite_master type filters (manifest query, legacy-object count, unknown-object report) so a view in the namespace can no longer hide from verification. - fix stale docs naming retired tables: agent_inbox/schema.rs module and init docs, agent_org_tasks module and init docs, core_types::tool_names ORG_SEND_MESSAGE, e2e-test member-idle probe. - doc-comment every module-level pub fn init_schema as tests-only — production initialization goes through the namespace coordinator. --- .../src/core/coordination/agent_inbox/schema.rs | 11 ++++++----- .../core/coordination/agent_member_interventions.rs | 3 +++ .../core/coordination/agent_org_plan_approvals/mod.rs | 3 +++ .../src/core/coordination/agent_org_runs/mod.rs | 7 +++++-- .../src/core/coordination/agent_org_tasks/mod.rs | 8 ++++++-- .../core/coordination/agent_org_watchdog/budget.rs | 3 +++ .../crates/agent-core/src/core/coordination/schema.rs | 6 +++--- src-tauri/crates/e2e-test/src/agent_org.rs | 2 +- src-tauri/crates/types/src/tool_names.rs | 2 +- 9 files changed, 31 insertions(+), 14 deletions(-) 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 c56bb6f9b2..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. @@ -83,11 +87,8 @@ pub(crate) fn create_schema(conn: &Connection) -> SqliteResult<()> { THEN payload_json ELSE '{{}}' END, '$.task_id' )='text'; - DROP INDEX IF EXISTS idx_agent_org_runtime_inbox_run_task_assignment_v3; - DROP INDEX IF EXISTS idx_agent_org_runtime_inbox_run_task_assignment_v2; CREATE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_request_id ON agent_org_runtime_inbox(request_id); - DROP INDEX IF EXISTS idx_agent_org_runtime_inbox_causation_once; CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_org_runtime_inbox_causation_recipient_once ON agent_org_runtime_inbox( causation_inbox_id, 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 4b1023b174..1ca8909b14 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 @@ -56,6 +56,9 @@ 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) } 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 f48e937114..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,6 +148,9 @@ 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) } 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 75f897619e..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 @@ -388,8 +388,11 @@ impl AgentOrgStartingFailure { } /// Initialize the redesigned runtime run envelope in an already-isolated -/// namespace. Production startup uses the complete schema coordinator; this -/// narrower entry point remains available to focused unit tests. +/// 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<()> { create_schema(conn) } 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 81f453ed9d..2f7649d94e 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 @@ -646,7 +646,11 @@ 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 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 51a3a3ea4d..586027da9c 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,6 +6,9 @@ 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) } diff --git a/src-tauri/crates/agent-core/src/core/coordination/schema.rs b/src-tauri/crates/agent-core/src/core/coordination/schema.rs index 53cc797c52..0a6bf89eac 100644 --- a/src-tauri/crates/agent-core/src/core/coordination/schema.rs +++ b/src-tauri/crates/agent-core/src/core/coordination/schema.rs @@ -50,7 +50,7 @@ const LEGACY_TABLES: [&str; 13] = [ 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') + AND type IN ('table', 'index', 'trigger', 'view') AND (name LIKE 'agent_org_runtime_%' OR tbl_name LIKE 'agent_org_runtime_%') ORDER BY type, name"; @@ -193,7 +193,7 @@ fn count_known_tables(conn: &Connection, names: &[&str]) -> SqliteResult 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')", + WHERE type IN ('table', 'index', 'trigger', 'view')", )?; let rows = statement.query_map([], |row| { Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) @@ -211,7 +211,7 @@ fn count_legacy_objects(conn: &Connection) -> SqliteResult { fn unknown_agent_org_objects(conn: &Connection) -> SqliteResult> { let mut statement = conn.prepare( "SELECT name FROM sqlite_master - WHERE type IN ('table', 'index', 'trigger') + 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", )?; 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/types/src/tool_names.rs b/src-tauri/crates/types/src/tool_names.rs index c911384f3d..17888ddeda 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 ───────────────────────────────────────────────── From d82ae0376efa429c1d371bd4d37dfb799299aa99 Mon Sep 17 00:00:00 2001 From: Shibo Sheng Date: Mon, 17 Aug 2026 01:09:40 +0800 Subject: [PATCH 3/3] test(agent-org): use canonical initial input table --- .../agent-core/src/core/coordination/agent_org_runs/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5289da03da..c997ebf965 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 @@ -397,7 +397,7 @@ fn starting_store_errors_drive_stable_permanent_failure_classification() { database::db::get_connection() .expect("db") .execute( - "DELETE FROM agent_org_initial_inputs WHERE org_run_id=?1", + "DELETE FROM agent_org_runtime_initial_inputs WHERE org_run_id=?1", [&run.id], ) .expect("remove the required initial-input certificate");