From ebb5805c428599644972b5a81a2cfa05280cfc93 Mon Sep 17 00:00:00 2001 From: eba8 Date: Fri, 1 May 2026 14:47:16 +0000 Subject: [PATCH 1/3] fix: sort startup sessions by normalized timestamps --- .../session_reads/startup_recovery.rs | 34 +++++++++--- .../src/infrastructure/persistence/tests.rs | 53 +++++++++++++++++++ 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/orbitdock-server/crates/server/src/infrastructure/persistence/session_reads/startup_recovery.rs b/orbitdock-server/crates/server/src/infrastructure/persistence/session_reads/startup_recovery.rs index e3f1fc04..aafe6129 100644 --- a/orbitdock-server/crates/server/src/infrastructure/persistence/session_reads/startup_recovery.rs +++ b/orbitdock-server/crates/server/src/infrastructure/persistence/session_reads/startup_recovery.rs @@ -4,6 +4,8 @@ use std::path::PathBuf; use rusqlite::OptionalExtension; use rusqlite::{params, Connection}; +use crate::support::session_time::parse_unix_z; + use super::super::chrono_now; use super::super::messages::load_messages_from_db; use super::super::transcripts::load_messages_from_transcript; @@ -14,6 +16,16 @@ use super::projections::{ActiveSessionRow, RestoredSessionParts}; use super::RestoredSession; use orbitdock_protocol::SessionControlMode; +fn parse_timestamp_to_unix(value: Option<&str>) -> Option { + let raw = value?; + if let Some(unix) = parse_unix_z(Some(raw)) { + return Some(unix); + } + chrono::DateTime::parse_from_rfc3339(raw) + .ok() + .map(|parsed| parsed.timestamp().max(0) as u64) +} + #[cfg(test)] pub async fn load_session_lifecycle_state( id: &str, @@ -237,13 +249,8 @@ async fn load_sessions_for_startup_with_db_path( LEFT JOIN usage_session_state uss ON uss.session_id = s.id WHERE (s.status = 'active') OR (s.status = 'ended' AND s.end_reason = 'server_shutdown') - ORDER BY - COALESCE( - CAST(REPLACE(s.last_progress_at, 'Z', '') AS INTEGER), - CAST(REPLACE(s.last_activity_at, 'Z', '') AS INTEGER), - 0 - ) DESC, - COALESCE(CAST(REPLACE(s.last_activity_at, 'Z', '') AS INTEGER), 0) DESC", + ORDER BY CASE s.status WHEN 'active' THEN 0 ELSE 1 END, + COALESCE(s.last_progress_at, s.last_activity_at, s.started_at, '') DESC", )?; let session_rows: Vec = stmt @@ -376,6 +383,19 @@ async fn load_sessions_for_startup_with_db_path( })); } + sessions.sort_by(|left, right| { + let left_progress = parse_timestamp_to_unix(left.last_progress_at.as_deref()); + let right_progress = parse_timestamp_to_unix(right.last_progress_at.as_deref()); + let left_activity = parse_timestamp_to_unix(left.last_activity_at.as_deref()) + .or_else(|| parse_timestamp_to_unix(left.started_at.as_deref())); + let right_activity = parse_timestamp_to_unix(right.last_activity_at.as_deref()) + .or_else(|| parse_timestamp_to_unix(right.started_at.as_deref())); + + right_progress + .cmp(&left_progress) + .then_with(|| right_activity.cmp(&left_activity)) + }); + Ok(sessions) }) .await??; diff --git a/orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs b/orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs index 466f4e7e..80eb91bd 100644 --- a/orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs +++ b/orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs @@ -1172,6 +1172,59 @@ fn startup_restore_only_ends_stale_passive_codex_sessions() { assert_eq!(approval_status.1, "permission"); } +#[test] +fn startup_restore_sorts_mixed_timestamp_formats_by_actual_recency() { + let (conn, db_path, _dir, _guard) = setup_test_db(); + + conn + .execute( + "INSERT INTO sessions ( + id, provider, status, work_status, lifecycle_state, control_mode, + project_path, codex_integration_mode, started_at, last_activity_at, last_progress_at + ) VALUES ( + 'iso-recent', 'codex', 'active', 'waiting', 'open', 'direct', + '/tmp/test', 'direct', '2026-05-01T14:00:00Z', '2026-05-01T14:10:34Z', '2026-05-01T14:10:34Z' + )", + [], + ) + .unwrap(); + conn + .execute( + "INSERT INTO sessions ( + id, provider, status, work_status, lifecycle_state, control_mode, + project_path, codex_integration_mode, started_at, last_activity_at, last_progress_at + ) VALUES ( + 'unix-stale', 'codex', 'active', 'waiting', 'open', 'direct', + '/tmp/test', 'direct', '2026-04-16T20:34:39Z', '1776438552Z', '1776438552Z' + )", + [], + ) + .unwrap(); + drop(conn); + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let restored = runtime + .block_on(super::session_reads::load_sessions_for_startup_from_db_path(db_path)) + .unwrap(); + + let iso_index = restored + .iter() + .position(|session| session.id == "iso-recent") + .expect("iso session should be restored"); + let unix_index = restored + .iter() + .position(|session| session.id == "unix-stale") + .expect("unix session should be restored"); + + assert!( + iso_index < unix_index, + "RFC3339 timestamps should sort ahead of older unix-with-Z timestamps" + ); +} + #[test] fn startup_restore_marks_open_direct_sessions_as_resumable() { let (conn, db_path, _dir, _guard) = setup_test_db(); From f24602ec8a08fac8051a4d6845cd4c1f961de084 Mon Sep 17 00:00:00 2001 From: eba8 Date: Fri, 1 May 2026 15:05:10 +0000 Subject: [PATCH 2/3] test: keep startup ordering fixtures resumable --- .../crates/server/src/infrastructure/persistence/tests.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs b/orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs index 80eb91bd..abc08fc4 100644 --- a/orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs +++ b/orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs @@ -1180,10 +1180,10 @@ fn startup_restore_sorts_mixed_timestamp_formats_by_actual_recency() { .execute( "INSERT INTO sessions ( id, provider, status, work_status, lifecycle_state, control_mode, - project_path, codex_integration_mode, started_at, last_activity_at, last_progress_at + project_path, codex_integration_mode, codex_thread_id, started_at, last_activity_at, last_progress_at ) VALUES ( 'iso-recent', 'codex', 'active', 'waiting', 'open', 'direct', - '/tmp/test', 'direct', '2026-05-01T14:00:00Z', '2026-05-01T14:10:34Z', '2026-05-01T14:10:34Z' + '/tmp/test', 'direct', 'thread-iso-recent', '2026-05-01T14:00:00Z', '2026-05-01T14:10:34Z', '2026-05-01T14:10:34Z' )", [], ) @@ -1192,10 +1192,10 @@ fn startup_restore_sorts_mixed_timestamp_formats_by_actual_recency() { .execute( "INSERT INTO sessions ( id, provider, status, work_status, lifecycle_state, control_mode, - project_path, codex_integration_mode, started_at, last_activity_at, last_progress_at + project_path, codex_integration_mode, codex_thread_id, started_at, last_activity_at, last_progress_at ) VALUES ( 'unix-stale', 'codex', 'active', 'waiting', 'open', 'direct', - '/tmp/test', 'direct', '2026-04-16T20:34:39Z', '1776438552Z', '1776438552Z' + '/tmp/test', 'direct', 'thread-unix-stale', '2026-04-16T20:34:39Z', '1776438552Z', '1776438552Z' )", [], ) From c87f35eb287b8373cbf58dc5b7978f82b540316f Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Mon, 4 May 2026 23:30:13 -0500 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20Finish=20startup=20ordering?= =?UTF-8?q?=20merge=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use started_at as the final startup restore tiebreaker, keep the merged Codex router helper in rustfmt shape, and bump the Rust CI cache key so stale target artifacts do not poison GitHub runs. --- .github/workflows/rust-ci.yml | 2 +- .../connector-codex/src/app_server/router.rs | 5 +- .../session_reads/startup_recovery.rs | 9 ++-- .../src/infrastructure/persistence/tests.rs | 53 +++++++++++++++++++ 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index dfcd3afe..01c23cd9 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -39,7 +39,7 @@ jobs: workspaces: "orbitdock-server -> ../.cache/rust/target" # Include workflow + Makefile in the key so cache busts on CI # or build-config changes, not just dependency changes. - shared-key: "rust-ci" + shared-key: "rust-ci-v2" cache-on-failure: true - name: rust fmt check diff --git a/orbitdock-server/crates/connector-codex/src/app_server/router.rs b/orbitdock-server/crates/connector-codex/src/app_server/router.rs index f1dcd0f9..4da10b63 100644 --- a/orbitdock-server/crates/connector-codex/src/app_server/router.rs +++ b/orbitdock-server/crates/connector-codex/src/app_server/router.rs @@ -52,10 +52,7 @@ pub(super) fn request_thread_id(request: &ServerRequest) -> Option { }) } -pub(super) async fn send_outputs( - route: &AppServerSessionRoute, - outputs: Vec, -) { +pub(super) async fn send_outputs(route: &AppServerSessionRoute, outputs: Vec) { for output in outputs { if route.forward_tx.send(output).is_err() { tracing::debug!("Typed codex app-server output channel closed"); diff --git a/orbitdock-server/crates/server/src/infrastructure/persistence/session_reads/startup_recovery.rs b/orbitdock-server/crates/server/src/infrastructure/persistence/session_reads/startup_recovery.rs index aafe6129..7544eae3 100644 --- a/orbitdock-server/crates/server/src/infrastructure/persistence/session_reads/startup_recovery.rs +++ b/orbitdock-server/crates/server/src/infrastructure/persistence/session_reads/startup_recovery.rs @@ -386,14 +386,15 @@ async fn load_sessions_for_startup_with_db_path( sessions.sort_by(|left, right| { let left_progress = parse_timestamp_to_unix(left.last_progress_at.as_deref()); let right_progress = parse_timestamp_to_unix(right.last_progress_at.as_deref()); - let left_activity = parse_timestamp_to_unix(left.last_activity_at.as_deref()) - .or_else(|| parse_timestamp_to_unix(left.started_at.as_deref())); - let right_activity = parse_timestamp_to_unix(right.last_activity_at.as_deref()) - .or_else(|| parse_timestamp_to_unix(right.started_at.as_deref())); + let left_activity = parse_timestamp_to_unix(left.last_activity_at.as_deref()); + let right_activity = parse_timestamp_to_unix(right.last_activity_at.as_deref()); + let left_started = parse_timestamp_to_unix(left.started_at.as_deref()); + let right_started = parse_timestamp_to_unix(right.started_at.as_deref()); right_progress .cmp(&left_progress) .then_with(|| right_activity.cmp(&left_activity)) + .then_with(|| right_started.cmp(&left_started)) }); Ok(sessions) diff --git a/orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs b/orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs index abc08fc4..ff32a270 100644 --- a/orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs +++ b/orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs @@ -1225,6 +1225,59 @@ fn startup_restore_sorts_mixed_timestamp_formats_by_actual_recency() { ); } +#[test] +fn startup_restore_uses_started_at_as_final_tiebreaker() { + let (conn, db_path, _dir, _guard) = setup_test_db(); + + conn + .execute( + "INSERT INTO sessions ( + id, provider, status, work_status, lifecycle_state, control_mode, + project_path, codex_integration_mode, codex_thread_id, started_at, last_activity_at, last_progress_at + ) VALUES ( + 'newer-start', 'codex', 'active', 'waiting', 'open', 'direct', + '/tmp/test', 'direct', 'thread-newer-start', '2026-05-01T14:11:00Z', '2026-05-01T14:10:34Z', '2026-05-01T14:10:34Z' + )", + [], + ) + .unwrap(); + conn + .execute( + "INSERT INTO sessions ( + id, provider, status, work_status, lifecycle_state, control_mode, + project_path, codex_integration_mode, codex_thread_id, started_at, last_activity_at, last_progress_at + ) VALUES ( + 'older-start', 'codex', 'active', 'waiting', 'open', 'direct', + '/tmp/test', 'direct', 'thread-older-start', '2026-05-01T14:09:00Z', '2026-05-01T14:10:34Z', '2026-05-01T14:10:34Z' + )", + [], + ) + .unwrap(); + drop(conn); + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let restored = runtime + .block_on(super::session_reads::load_sessions_for_startup_from_db_path(db_path)) + .unwrap(); + + let newer_index = restored + .iter() + .position(|session| session.id == "newer-start") + .expect("newer-start session should be restored"); + let older_index = restored + .iter() + .position(|session| session.id == "older-start") + .expect("older-start session should be restored"); + + assert!( + newer_index < older_index, + "started_at should break ties after progress and activity timestamps" + ); +} + #[test] fn startup_restore_marks_open_direct_sessions_as_resumable() { let (conn, db_path, _dir, _guard) = setup_test_db();