Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ pub(super) fn request_thread_id(request: &ServerRequest) -> Option<String> {
})
}

pub(super) async fn send_outputs(
route: &AppServerSessionRoute,
outputs: Vec<ConnectorOutput>,
) {
pub(super) async fn send_outputs(route: &AppServerSessionRoute, outputs: Vec<ConnectorOutput>) {
for output in outputs {
if route.forward_tx.send(output).is_err() {
tracing::debug!("Typed codex app-server output channel closed");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<u64> {
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,
Expand Down Expand Up @@ -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<ActiveSessionRow> = stmt
Expand Down Expand Up @@ -376,6 +383,20 @@ 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());
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)
})
.await??;
Expand Down
106 changes: 106 additions & 0 deletions orbitdock-server/crates/server/src/infrastructure/persistence/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,112 @@ 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, codex_thread_id, started_at, last_activity_at, last_progress_at
) VALUES (
'iso-recent', 'codex', 'active', 'waiting', 'open', 'direct',
'/tmp/test', 'direct', 'thread-iso-recent', '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, codex_thread_id, started_at, last_activity_at, last_progress_at
) VALUES (
'unix-stale', 'codex', 'active', 'waiting', 'open', 'direct',
'/tmp/test', 'direct', 'thread-unix-stale', '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_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();
Expand Down
Loading