diff --git a/crates/db/.sqlx/query-01a0f9724e5fce7d3312a742e72cded85605ee540150972e2a8364919f56d5c0.json b/crates/db/.sqlx/query-01a0f9724e5fce7d3312a742e72cded85605ee540150972e2a8364919f56d5c0.json index 9971636d..26076bd5 100644 --- a/crates/db/.sqlx/query-01a0f9724e5fce7d3312a742e72cded85605ee540150972e2a8364919f56d5c0.json +++ b/crates/db/.sqlx/query-01a0f9724e5fce7d3312a742e72cded85605ee540150972e2a8364919f56d5c0.json @@ -46,12 +46,12 @@ { "name": "has_in_progress_attempt!: i64", "ordinal": 8, - "type_info": "Integer" + "type_info": "Null" }, { "name": "last_attempt_failed!: i64", "ordinal": 9, - "type_info": "Integer" + "type_info": "Null" }, { "name": "executor!: String", @@ -71,8 +71,8 @@ true, false, false, - false, - false, + null, + null, true ] }, diff --git a/crates/db/.sqlx/query-4b84758f0eef3e0abc3ec8b26528987d3482554da67e2aa944d0099d9e1c6f64.json b/crates/db/.sqlx/query-09d997b7b3dcc6bbea9b20c878795f6d13bac6f4f9064c457f2e4847a76214be.json similarity index 68% rename from crates/db/.sqlx/query-4b84758f0eef3e0abc3ec8b26528987d3482554da67e2aa944d0099d9e1c6f64.json rename to crates/db/.sqlx/query-09d997b7b3dcc6bbea9b20c878795f6d13bac6f4f9064c457f2e4847a76214be.json index 0dbf5c35..801f0347 100644 --- a/crates/db/.sqlx/query-4b84758f0eef3e0abc3ec8b26528987d3482554da67e2aa944d0099d9e1c6f64.json +++ b/crates/db/.sqlx/query-09d997b7b3dcc6bbea9b20c878795f6d13bac6f4f9064c457f2e4847a76214be.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT es.session_id\n FROM execution_processes ep\n JOIN executor_sessions es ON ep.id = es.execution_process_id \n WHERE ep.task_attempt_id = $1\n AND ep.run_reason = 'codingagent'\n AND ep.dropped = 0\n AND es.session_id IS NOT NULL\n ORDER BY ep.created_at DESC\n LIMIT 1", + "query": "SELECT es.session_id\n FROM execution_processes ep\n JOIN executor_sessions es ON ep.id = es.execution_process_id \n WHERE ep.task_attempt_id = $1\n AND ep.run_reason = 'codingagent'\n AND ep.dropped = FALSE\n AND es.session_id IS NOT NULL\n ORDER BY ep.created_at DESC\n LIMIT 1", "describe": { "columns": [ { @@ -16,5 +16,5 @@ true ] }, - "hash": "4b84758f0eef3e0abc3ec8b26528987d3482554da67e2aa944d0099d9e1c6f64" + "hash": "09d997b7b3dcc6bbea9b20c878795f6d13bac6f4f9064c457f2e4847a76214be" } diff --git a/crates/db/.sqlx/query-0a6ebe48540eb694056f10b888beb95e933ca3dcfff9b1a96aa333f4743d697d.json b/crates/db/.sqlx/query-0a6ebe48540eb694056f10b888beb95e933ca3dcfff9b1a96aa333f4743d697d.json deleted file mode 100644 index 2dac9ca8..00000000 --- a/crates/db/.sqlx/query-0a6ebe48540eb694056f10b888beb95e933ca3dcfff9b1a96aa333f4743d697d.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE execution_processes\n SET dropped = 1\n WHERE task_attempt_id = $1\n AND created_at >= (SELECT created_at FROM execution_processes WHERE id = $2)\n AND dropped = 0", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "0a6ebe48540eb694056f10b888beb95e933ca3dcfff9b1a96aa333f4743d697d" -} diff --git a/crates/db/.sqlx/query-7eaddb41d66a5feeb0cadb2757513589f228261cd6b87b8707f04a482fe1f880.json b/crates/db/.sqlx/query-1c652bb5d039cdcef8e5cc64e283771b6e49fdf3abea89652d2bc57dafd2c63d.json similarity index 61% rename from crates/db/.sqlx/query-7eaddb41d66a5feeb0cadb2757513589f228261cd6b87b8707f04a482fe1f880.json rename to crates/db/.sqlx/query-1c652bb5d039cdcef8e5cc64e283771b6e49fdf3abea89652d2bc57dafd2c63d.json index 6b15d396..2b2d3914 100644 --- a/crates/db/.sqlx/query-7eaddb41d66a5feeb0cadb2757513589f228261cd6b87b8707f04a482fe1f880.json +++ b/crates/db/.sqlx/query-1c652bb5d039cdcef8e5cc64e283771b6e49fdf3abea89652d2bc57dafd2c63d.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id as \"id!: Uuid\", task_attempt_id as \"task_attempt_id!: Uuid\", run_reason as \"run_reason!: ExecutionProcessRunReason\", executor_action as \"executor_action!: sqlx::types::Json\", before_head_commit,\n after_head_commit, status as \"status!: ExecutionProcessStatus\", exit_code, dropped, started_at as \"started_at!: DateTime\", completed_at as \"completed_at?: DateTime\",\n created_at as \"created_at!: DateTime\", updated_at as \"updated_at!: DateTime\"\n FROM execution_processes WHERE task_attempt_id = ? ORDER BY created_at ASC", + "query": "SELECT id as \"id!: Uuid\",\n task_attempt_id as \"task_attempt_id!: Uuid\",\n run_reason as \"run_reason!: ExecutionProcessRunReason\",\n executor_action as \"executor_action!: sqlx::types::Json\",\n before_head_commit,\n after_head_commit,\n status as \"status!: ExecutionProcessStatus\",\n exit_code,\n dropped,\n started_at as \"started_at!: DateTime\",\n completed_at as \"completed_at?: DateTime\",\n created_at as \"created_at!: DateTime\",\n updated_at as \"updated_at!: DateTime\"\n FROM execution_processes\n WHERE task_attempt_id = ?\n AND (? OR dropped = FALSE)\n ORDER BY created_at ASC", "describe": { "columns": [ { @@ -70,7 +70,7 @@ } ], "parameters": { - "Right": 1 + "Right": 2 }, "nullable": [ true, @@ -88,5 +88,5 @@ false ] }, - "hash": "7eaddb41d66a5feeb0cadb2757513589f228261cd6b87b8707f04a482fe1f880" + "hash": "1c652bb5d039cdcef8e5cc64e283771b6e49fdf3abea89652d2bc57dafd2c63d" } diff --git a/crates/db/.sqlx/query-290fb7c65611a73e2b3955383c5881d47b101ffec11415ea6824e390d478921f.json b/crates/db/.sqlx/query-290fb7c65611a73e2b3955383c5881d47b101ffec11415ea6824e390d478921f.json new file mode 100644 index 00000000..6a3870be --- /dev/null +++ b/crates/db/.sqlx/query-290fb7c65611a73e2b3955383c5881d47b101ffec11415ea6824e390d478921f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE execution_processes\n SET dropped = TRUE\n WHERE task_attempt_id = $1\n AND created_at >= (SELECT created_at FROM execution_processes WHERE id = $2)\n AND dropped = FALSE", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "290fb7c65611a73e2b3955383c5881d47b101ffec11415ea6824e390d478921f" +} diff --git a/crates/db/.sqlx/query-09643a63fec26ce20ce0d1d4908cad117c28efee3c45a7a4c422125432680643.json b/crates/db/.sqlx/query-3880c8745b172bec8e9f0477ee78339e157531a00dc63d73a31ce554f54e5ca6.json similarity index 93% rename from crates/db/.sqlx/query-09643a63fec26ce20ce0d1d4908cad117c28efee3c45a7a4c422125432680643.json rename to crates/db/.sqlx/query-3880c8745b172bec8e9f0477ee78339e157531a00dc63d73a31ce554f54e5ca6.json index 6b7f4709..723993cb 100644 --- a/crates/db/.sqlx/query-09643a63fec26ce20ce0d1d4908cad117c28efee3c45a7a4c422125432680643.json +++ b/crates/db/.sqlx/query-3880c8745b172bec8e9f0477ee78339e157531a00dc63d73a31ce554f54e5ca6.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id as \"id!: Uuid\", task_attempt_id as \"task_attempt_id!: Uuid\", run_reason as \"run_reason!: ExecutionProcessRunReason\", executor_action as \"executor_action!: sqlx::types::Json\", before_head_commit,\n after_head_commit, status as \"status!: ExecutionProcessStatus\", exit_code, dropped, started_at as \"started_at!: DateTime\", completed_at as \"completed_at?: DateTime\",\n created_at as \"created_at!: DateTime\", updated_at as \"updated_at!: DateTime\"\n FROM execution_processes\n WHERE task_attempt_id = ? AND run_reason = ? AND dropped = 0\n ORDER BY created_at DESC LIMIT 1", + "query": "SELECT id as \"id!: Uuid\", task_attempt_id as \"task_attempt_id!: Uuid\", run_reason as \"run_reason!: ExecutionProcessRunReason\", executor_action as \"executor_action!: sqlx::types::Json\", before_head_commit,\n after_head_commit, status as \"status!: ExecutionProcessStatus\", exit_code, dropped, started_at as \"started_at!: DateTime\", completed_at as \"completed_at?: DateTime\",\n created_at as \"created_at!: DateTime\", updated_at as \"updated_at!: DateTime\"\n FROM execution_processes\n WHERE task_attempt_id = ? AND run_reason = ? AND dropped = FALSE\n ORDER BY created_at DESC LIMIT 1", "describe": { "columns": [ { @@ -88,5 +88,5 @@ false ] }, - "hash": "09643a63fec26ce20ce0d1d4908cad117c28efee3c45a7a4c422125432680643" + "hash": "3880c8745b172bec8e9f0477ee78339e157531a00dc63d73a31ce554f54e5ca6" } diff --git a/crates/db/.sqlx/query-453c1691f4f8109931a4b0dc95e2153645bbc2d315d76f371612a7b6262a1c21.json b/crates/db/.sqlx/query-453c1691f4f8109931a4b0dc95e2153645bbc2d315d76f371612a7b6262a1c21.json deleted file mode 100644 index 1b0370bf..00000000 --- a/crates/db/.sqlx/query-453c1691f4f8109931a4b0dc95e2153645bbc2d315d76f371612a7b6262a1c21.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE execution_processes\n SET dropped = 1\n WHERE task_attempt_id = $1\n AND created_at > (SELECT created_at FROM execution_processes WHERE id = $2)\n AND dropped = 0\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "453c1691f4f8109931a4b0dc95e2153645bbc2d315d76f371612a7b6262a1c21" -} diff --git a/crates/db/.sqlx/query-b170ff05e4526f2f97fe132c72a5433a29702e33074bd8c563d9a8eaa78cf9ad.json b/crates/db/.sqlx/query-b170ff05e4526f2f97fe132c72a5433a29702e33074bd8c563d9a8eaa78cf9ad.json new file mode 100644 index 00000000..2cf8c516 --- /dev/null +++ b/crates/db/.sqlx/query-b170ff05e4526f2f97fe132c72a5433a29702e33074bd8c563d9a8eaa78cf9ad.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE execution_processes\n SET dropped = TRUE\n WHERE task_attempt_id = $1\n AND created_at > (SELECT created_at FROM execution_processes WHERE id = $2)\n AND dropped = FALSE\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "b170ff05e4526f2f97fe132c72a5433a29702e33074bd8c563d9a8eaa78cf9ad" +} diff --git a/crates/db/src/models/execution_process.rs b/crates/db/src/models/execution_process.rs index 81660cdf..6bd80aae 100644 --- a/crates/db/src/models/execution_process.rs +++ b/crates/db/src/models/execution_process.rs @@ -195,18 +195,33 @@ impl ExecutionProcess { .await } - /// Find all execution processes for a task attempt + /// Find all execution processes for a task attempt (optionally include soft-deleted) pub async fn find_by_task_attempt_id( pool: &SqlitePool, task_attempt_id: Uuid, + show_soft_deleted: bool, ) -> Result, sqlx::Error> { sqlx::query_as!( ExecutionProcess, - r#"SELECT id as "id!: Uuid", task_attempt_id as "task_attempt_id!: Uuid", run_reason as "run_reason!: ExecutionProcessRunReason", executor_action as "executor_action!: sqlx::types::Json", before_head_commit, - after_head_commit, status as "status!: ExecutionProcessStatus", exit_code, dropped, started_at as "started_at!: DateTime", completed_at as "completed_at?: DateTime", - created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" - FROM execution_processes WHERE task_attempt_id = ? ORDER BY created_at ASC"#, - task_attempt_id + r#"SELECT id as "id!: Uuid", + task_attempt_id as "task_attempt_id!: Uuid", + run_reason as "run_reason!: ExecutionProcessRunReason", + executor_action as "executor_action!: sqlx::types::Json", + before_head_commit, + after_head_commit, + status as "status!: ExecutionProcessStatus", + exit_code, + dropped, + started_at as "started_at!: DateTime", + completed_at as "completed_at?: DateTime", + created_at as "created_at!: DateTime", + updated_at as "updated_at!: DateTime" + FROM execution_processes + WHERE task_attempt_id = ? + AND (? OR dropped = FALSE) + ORDER BY created_at ASC"#, + task_attempt_id, + show_soft_deleted ) .fetch_all(pool) .await @@ -261,7 +276,7 @@ impl ExecutionProcess { JOIN executor_sessions es ON ep.id = es.execution_process_id WHERE ep.task_attempt_id = $1 AND ep.run_reason = 'codingagent' - AND ep.dropped = 0 + AND ep.dropped = FALSE AND es.session_id IS NOT NULL ORDER BY ep.created_at DESC LIMIT 1"#, @@ -287,7 +302,7 @@ impl ExecutionProcess { after_head_commit, status as "status!: ExecutionProcessStatus", exit_code, dropped, started_at as "started_at!: DateTime", completed_at as "completed_at?: DateTime", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" FROM execution_processes - WHERE task_attempt_id = ? AND run_reason = ? AND dropped = 0 + WHERE task_attempt_id = ? AND run_reason = ? AND dropped = FALSE ORDER BY created_at DESC LIMIT 1"#, task_attempt_id, run_reason @@ -433,10 +448,10 @@ impl ExecutionProcess { // Monotonic drop: only mark newer records as dropped; never undrop. sqlx::query!( r#"UPDATE execution_processes - SET dropped = 1 + SET dropped = TRUE WHERE task_attempt_id = $1 AND created_at > (SELECT created_at FROM execution_processes WHERE id = $2) - AND dropped = 0 + AND dropped = FALSE "#, task_attempt_id, boundary_process_id @@ -454,10 +469,10 @@ impl ExecutionProcess { ) -> Result { let result = sqlx::query!( r#"UPDATE execution_processes - SET dropped = 1 + SET dropped = TRUE WHERE task_attempt_id = $1 AND created_at >= (SELECT created_at FROM execution_processes WHERE id = $2) - AND dropped = 0"#, + AND dropped = FALSE"#, task_attempt_id, boundary_process_id ) diff --git a/crates/local-deployment/src/container.rs b/crates/local-deployment/src/container.rs index 593c1d05..0b2572a1 100644 --- a/crates/local-deployment/src/container.rs +++ b/crates/local-deployment/src/container.rs @@ -1228,7 +1228,8 @@ impl LocalContainerService { // If anything is running for this attempt, bail let procs = - ExecutionProcess::find_by_task_attempt_id(&self.db.pool, ctx.task_attempt.id).await?; + ExecutionProcess::find_by_task_attempt_id(&self.db.pool, ctx.task_attempt.id, false) + .await?; if procs .iter() .any(|p| matches!(p.status, ExecutionProcessStatus::Running)) diff --git a/crates/server/src/routes/execution_processes.rs b/crates/server/src/routes/execution_processes.rs index 300bd2a3..131da892 100644 --- a/crates/server/src/routes/execution_processes.rs +++ b/crates/server/src/routes/execution_processes.rs @@ -22,6 +22,9 @@ use crate::{DeploymentImpl, error::ApiError, middleware::load_execution_process_ #[derive(Debug, Deserialize)] pub struct ExecutionProcessQuery { pub task_attempt_id: Uuid, + /// If true, include soft-deleted (dropped) processes in results/stream + #[serde(default)] + pub show_soft_deleted: Option, } pub async fn get_execution_processes( @@ -29,8 +32,12 @@ pub async fn get_execution_processes( Query(query): Query, ) -> Result>>, ApiError> { let pool = &deployment.db().pool; - let execution_processes = - ExecutionProcess::find_by_task_attempt_id(pool, query.task_attempt_id).await?; + let execution_processes = ExecutionProcess::find_by_task_attempt_id( + pool, + query.task_attempt_id, + query.show_soft_deleted.unwrap_or(false), + ) + .await?; Ok(ResponseJson(ApiResponse::success(execution_processes))) } @@ -189,8 +196,13 @@ pub async fn stream_execution_processes_ws( Query(query): Query, ) -> impl IntoResponse { ws.on_upgrade(move |socket| async move { - if let Err(e) = - handle_execution_processes_ws(socket, deployment, query.task_attempt_id).await + if let Err(e) = handle_execution_processes_ws( + socket, + deployment, + query.task_attempt_id, + query.show_soft_deleted.unwrap_or(false), + ) + .await { tracing::warn!("execution processes WS closed: {}", e); } @@ -201,11 +213,12 @@ async fn handle_execution_processes_ws( socket: WebSocket, deployment: DeploymentImpl, task_attempt_id: uuid::Uuid, + show_soft_deleted: bool, ) -> anyhow::Result<()> { // Get the raw stream and convert LogMsg to WebSocket messages let mut stream = deployment .events() - .stream_execution_processes_for_attempt_raw(task_attempt_id) + .stream_execution_processes_for_attempt_raw(task_attempt_id, show_soft_deleted) .await? .map_ok(|msg| msg.to_ws_message_unchecked()); diff --git a/crates/server/src/routes/task_attempts.rs b/crates/server/src/routes/task_attempts.rs index 4b1fe668..b9ed1ad8 100644 --- a/crates/server/src/routes/task_attempts.rs +++ b/crates/server/src/routes/task_attempts.rs @@ -328,7 +328,7 @@ async fn has_running_processes_for_attempt( pool: &sqlx::SqlitePool, attempt_id: Uuid, ) -> Result { - let processes = ExecutionProcess::find_by_task_attempt_id(pool, attempt_id).await?; + let processes = ExecutionProcess::find_by_task_attempt_id(pool, attempt_id, false).await?; Ok(processes.into_iter().any(|p| { matches!( p.status, @@ -602,18 +602,21 @@ pub async fn set_follow_up_queue( tokio::time::sleep(Duration::from_millis(1200)).await; let pool = &deployment_clone.db().pool; // Still no running process? - let running = - match ExecutionProcess::find_by_task_attempt_id(pool, task_attempt_clone.id) - .await - { - Ok(procs) => procs.into_iter().any(|p| { - matches!( - p.status, - db::models::execution_process::ExecutionProcessStatus::Running - ) - }), - Err(_) => true, // assume running on error to avoid duplicate starts - }; + let running = match ExecutionProcess::find_by_task_attempt_id( + pool, + task_attempt_clone.id, + false, + ) + .await + { + Ok(procs) => procs.into_iter().any(|p| { + matches!( + p.status, + db::models::execution_process::ExecutionProcessStatus::Running + ) + }), + Err(_) => true, // assume running on error to avoid duplicate starts + }; if running { return; } diff --git a/crates/services/src/services/container.rs b/crates/services/src/services/container.rs index 49e1bfed..505c95fa 100644 --- a/crates/services/src/services/container.rs +++ b/crates/services/src/services/container.rs @@ -120,7 +120,7 @@ pub trait ContainerService { for attempt in attempts { if let Ok(processes) = - ExecutionProcess::find_by_task_attempt_id(&self.db().pool, attempt.id).await + ExecutionProcess::find_by_task_attempt_id(&self.db().pool, attempt.id, false).await { for process in processes { if process.status == ExecutionProcessStatus::Running { @@ -147,7 +147,7 @@ pub trait ContainerService { async fn try_stop(&self, task_attempt: &TaskAttempt) { // stop all execution processes for this attempt if let Ok(processes) = - ExecutionProcess::find_by_task_attempt_id(&self.db().pool, task_attempt.id).await + ExecutionProcess::find_by_task_attempt_id(&self.db().pool, task_attempt.id, false).await { for process in processes { if process.status == ExecutionProcessStatus::Running { diff --git a/crates/services/src/services/events.rs b/crates/services/src/services/events.rs index b3981eb6..6d14326f 100644 --- a/crates/services/src/services/events.rs +++ b/crates/services/src/services/events.rs @@ -620,11 +620,16 @@ impl EventService { pub async fn stream_execution_processes_for_attempt_raw( &self, task_attempt_id: Uuid, + show_soft_deleted: bool, ) -> Result>, EventError> { - // Get initial snapshot of execution processes - let processes = - ExecutionProcess::find_by_task_attempt_id(&self.db.pool, task_attempt_id).await?; + // Get initial snapshot of execution processes (filtering at SQL level) + let processes = ExecutionProcess::find_by_task_attempt_id( + &self.db.pool, + task_attempt_id, + show_soft_deleted, + ) + .await?; // Convert processes array to object keyed by process ID let processes_map: serde_json::Map = processes @@ -662,6 +667,9 @@ impl EventService { ) && process.task_attempt_id == task_attempt_id { + if !show_soft_deleted && process.dropped { + return None; + } return Some(Ok(LogMsg::JsonPatch(patch))); } } @@ -673,6 +681,11 @@ impl EventService { ) && process.task_attempt_id == task_attempt_id { + if !show_soft_deleted && process.dropped { + let remove_patch = + execution_process_patch::remove(process.id); + return Some(Ok(LogMsg::JsonPatch(remove_patch))); + } return Some(Ok(LogMsg::JsonPatch(patch))); } } @@ -692,6 +705,11 @@ impl EventService { match &event_patch.value.record { RecordTypes::ExecutionProcess(process) => { if process.task_attempt_id == task_attempt_id { + if !show_soft_deleted && process.dropped { + let remove_patch = + execution_process_patch::remove(process.id); + return Some(Ok(LogMsg::JsonPatch(remove_patch))); + } return Some(Ok(LogMsg::JsonPatch(patch))); } } diff --git a/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx b/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx index 4a87f77f..7080826f 100644 --- a/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx +++ b/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx @@ -27,7 +27,7 @@ function ProcessesTab({ attemptId }: ProcessesTabProps) { isLoading: processesLoading, isConnected, error: processesError, - } = useExecutionProcesses(attemptId ?? ''); + } = useExecutionProcesses(attemptId ?? '', { showSoftDeleted: true }); const { selectedProcessId, setSelectedProcessId } = useProcessSelection(); const [loadingProcessId, setLoadingProcessId] = useState(null); const [localProcessDetails, setLocalProcessDetails] = useState< diff --git a/frontend/src/hooks/useConversationHistory.ts b/frontend/src/hooks/useConversationHistory.ts index 35f707d4..3f7a09c4 100644 --- a/frontend/src/hooks/useConversationHistory.ts +++ b/frontend/src/hooks/useConversationHistory.ts @@ -55,14 +55,7 @@ export const useConversationHistory = ({ const { executionProcesses: executionProcessesRaw } = useExecutionProcesses( attempt.id ); - // Soft-deleted (dropped) are invisible in the the conversation history - const visibleExecutionProcesses = useMemo( - () => executionProcessesRaw?.filter((p) => !p.dropped) ?? [], - [executionProcessesRaw] - ); - const executionProcesses = useRef( - visibleExecutionProcesses - ); + const executionProcesses = useRef(executionProcessesRaw); const displayedExecutionProcesses = useRef({}); const loadedInitialEntries = useRef(false); const lastRunningProcessId = useRef(null); @@ -73,8 +66,8 @@ export const useConversationHistory = ({ // Keep executionProcesses up to date useEffect(() => { - executionProcesses.current = visibleExecutionProcesses; - }, [visibleExecutionProcesses]); + executionProcesses.current = executionProcessesRaw; + }, [executionProcessesRaw]); const loadEntriesForHistoricExecutionProcess = ( executionProcess: ExecutionProcess @@ -408,8 +401,8 @@ export const useConversationHistory = ({ // Stable key for dependency arrays when process list changes const idListKey = useMemo( - () => visibleExecutionProcesses?.map((p) => p.id).join(','), - [visibleExecutionProcesses] + () => executionProcessesRaw?.map((p) => p.id).join(','), + [executionProcessesRaw] ); // Initial load when attempt changes @@ -459,11 +452,11 @@ export const useConversationHistory = ({ // If an execution process is removed, remove it from the state useEffect(() => { - if (!visibleExecutionProcesses) return; + if (!executionProcessesRaw) return; const removedProcessIds = Object.keys( displayedExecutionProcesses.current - ).filter((id) => !visibleExecutionProcesses.some((p) => p.id === id)); + ).filter((id) => !executionProcessesRaw.some((p) => p.id === id)); removedProcessIds.forEach((id) => { delete displayedExecutionProcesses.current[id]; diff --git a/frontend/src/hooks/useExecutionProcesses.ts b/frontend/src/hooks/useExecutionProcesses.ts index 3b3ca5e2..0388d3da 100644 --- a/frontend/src/hooks/useExecutionProcesses.ts +++ b/frontend/src/hooks/useExecutionProcesses.ts @@ -20,9 +20,15 @@ interface UseExecutionProcessesResult { * Live updates arrive at /execution_processes/ via add/replace/remove operations. */ export const useExecutionProcesses = ( - taskAttemptId: string + taskAttemptId: string, + opts?: { showSoftDeleted?: boolean } ): UseExecutionProcessesResult => { - const endpoint = `/api/execution-processes/stream/ws?task_attempt_id=${encodeURIComponent(taskAttemptId)}`; + const showSoftDeleted = opts?.showSoftDeleted; + const params = new URLSearchParams({ task_attempt_id: taskAttemptId }); + if (typeof showSoftDeleted === 'boolean') { + params.set('show_soft_deleted', String(showSoftDeleted)); + } + const endpoint = `/api/execution-processes/stream/ws?${params.toString()}`; const initialData = useCallback( (): ExecutionProcessState => ({ execution_processes: {} }),