Introduce boot_time to evaluate_state

The `boot_time` parameter is introduced to `evaluate_state` and its
callers. This parameter acts as a lower bound for the "seen at"
timestamp used in determining the `Idle` state for cases.

Previously, the `elapsed` duration was calculated as
`now.duration_since(latest_mtime)`. This meant that a server restart
could immediately cause old recordings to be classified as `Idle` if
their modification times were significantly in the past, potentially
triggering a large number of LLM calls upon the first render of the
`/cases` page.

By using `seen_at = std::cmp::max(latest_mtime, boot_time)`, the idle
calculation now ensures that a fresh server start respects a grace
period. Old recordings will not satisfy the idle threshold until the
elapsed time since the server's boot (or the recording's modification
time, whichever is later) exceeds the `idle_threshold`.

`SystemTime::UNIX_EPOCH` is used as the default `boot_time` in tests and
in `create_router_and_session_store_with_settings`. This preserves
existing test behavior where the idle grace period is effectively
disabled. For production, `main.rs` now captures the server's actual
boot time and passes it down.

This change also refactors `evaluate_case` and `try_enqueue` to accept
the new `boot_time` parameter. `try_enqueue_all_for_user` is updated to
pass `boot_time` along. `boot_scan_state` and `compute_state_for_user`
in `recovery.rs` also receive `boot_time`.

Additionally, a new test case
`analyze_required_endpoint_enqueues_only_required_cases` is added to
verify the behavior of the `/cases/analyze-required` endpoint. This
endpoint now correctly enqueues only `Required`
This commit is contained in:
2026-05-05 15:12:08 +02:00
parent bd133e9944
commit 16ef0bbe78
10 changed files with 392 additions and 41 deletions
+21
View File
@@ -87,6 +87,15 @@ pub struct PipelineState {
pub oneliner_locks: OnelinerLocks,
}
/// Newtype for the server's start timestamp. Functions as the lower
/// bound for the analyze idle-trigger: a case is only auto-enqueued
/// once `now - max(latest_recording_mtime, boot_time) >= threshold`.
/// Without this, a server restart would treat every case with an
/// older recording as immediately `Idle` and spawn a thundering herd
/// of LLM calls on the first `/cases` render.
#[derive(Clone, Copy, Debug)]
pub struct BootTime(pub std::time::SystemTime);
/// Shared application state. Clonable — all fields are cheap to clone
/// (`Arc` and `mpsc::Sender` / `broadcast::Sender`).
#[derive(Clone)]
@@ -104,6 +113,7 @@ pub struct AppState {
pub events_tx: events::EventSender,
pub http_client: reqwest::Client,
pub vocab: Arc<Gazetteer>,
pub boot_time: BootTime,
}
impl FromRef<AppState> for Arc<Config> {
@@ -197,6 +207,12 @@ impl FromRef<AppState> for Arc<Gazetteer> {
}
}
impl FromRef<AppState> for BootTime {
fn from_ref(state: &AppState) -> Self {
state.boot_time
}
}
/// Test/simple entrypoint: jobs pushed into either channel are dropped
/// because the receivers are not retained. Use [`create_router_with_state`]
/// from `main.rs` where real workers own the receivers.
@@ -242,6 +258,11 @@ pub fn create_router_and_session_store_with_settings(
events_tx: events::channel(),
http_client: reqwest::Client::new(),
vocab: Arc::new(Gazetteer::empty()),
// UNIX_EPOCH = "the server has always been up" — disables the
// post-boot grace window in tests, which is what most tests want
// anyway. Tests that exercise the grace-window itself construct
// an `AppState` with an explicit recent `BootTime`.
boot_time: BootTime(std::time::SystemTime::UNIX_EPOCH),
});
(router, session_store)
}