Skip to content

Commit a97b661

Browse files
committed
fix(infinity-daemon): reconcile persisted session statuses at startup
After a crash or `kill -9`, sessions persisted mid-flight in `sessions.json` (`idle: false, shut_down: false`) loaded as `Running` forever: the web UI showed green "running" dots for sessions with no live driver (4 of 56 sessions on a real store). * `SessionStore::reconcile_cold_start()` marks those entries idle right after load, before any client can connect, and the result is persisted. Stopped and archived entries are untouched; persisted pending choices still surface as `WaitingForChoice`, which ranks above `idle`. * Test: `restart_reconciles_stale_sessions` (stale running -> Idle and written back to disk, stopped stays Stopped).
1 parent 97dee40 commit a97b661

3 files changed

Lines changed: 83 additions & 4 deletions

File tree

‎crates/infinity-daemon/src/session/mod.rs‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,16 @@ impl SessionManager {
156156
let sessions_path = state_dir.join("sessions.json");
157157
let (change_tx, mut change_rx) = mpsc::unbounded_channel::<ThreadId>();
158158
let change_tx_for_conv = change_tx.clone();
159-
let session_store = Arc::new(tokio::sync::Mutex::new(session_store::SessionStore::load(
160-
&sessions_path.to_string_lossy(),
161-
change_tx,
162-
)));
159+
let session_store = {
160+
let mut store =
161+
session_store::SessionStore::load(&sessions_path.to_string_lossy(), change_tx);
162+
if store.reconcile_cold_start()
163+
&& let Err(e) = store.save()
164+
{
165+
tracing::error!("failed to persist reconciled session statuses: {e}");
166+
}
167+
Arc::new(tokio::sync::Mutex::new(store))
168+
};
163169
let broadcast_clients: Arc<std::sync::Mutex<Vec<mpsc::UnboundedSender<DaemonMessage>>>> =
164170
Arc::new(std::sync::Mutex::new(Vec::new()));
165171

‎crates/infinity-daemon/src/session/tests.rs‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,3 +778,60 @@ async fn shut_down_session_events_do_not_wake_threads() {
778778
})
779779
.await;
780780
}
781+
782+
/// A session persisted as running by a daemon that died mid-completion must
783+
/// come back Idle (and stay so on disk), not Running forever; an explicitly
784+
/// stopped session stays Stopped.
785+
#[tokio::test(flavor = "current_thread")]
786+
async fn restart_reconciles_stale_sessions() {
787+
let local = tokio::task::LocalSet::new();
788+
local
789+
.run_until(async {
790+
let (model, _ctrl) = mock_model();
791+
let entry = ModelEntry {
792+
model_id: "model1".to_owned(),
793+
display_name: "model1".to_owned(),
794+
context_window: 100_000,
795+
max_output_tokens: None,
796+
supports_image_input: false,
797+
};
798+
let state_dir = tempfile::tempdir().expect("create daemon state dir");
799+
let sessions_path = state_dir.path().join("sessions.json");
800+
let sessions = serde_json::json!({ "sessions": {
801+
"killed": { "cwd": "/tmp", "shut_down": false, "idle": false },
802+
"stopped": { "cwd": "/tmp", "shut_down": true, "idle": false },
803+
}});
804+
std::fs::write(&sessions_path, sessions.to_string()).expect("write sessions.json");
805+
806+
let manager = SessionManager::with_providers(
807+
SessionManagerConfig {
808+
state_dir: state_dir.path().to_path_buf(),
809+
callback_url: "http://127.0.0.1:0".to_owned(),
810+
user_rap_config: None,
811+
id_source: Arc::new(SequentialIdSource::new()),
812+
},
813+
vec![(
814+
"provider1".to_owned(),
815+
Arc::new(SingleModelProvider::new(entry, model)) as Arc<dyn ModelProvider>,
816+
)],
817+
vec![],
818+
)
819+
.await
820+
.expect("build session manager");
821+
822+
let sessions = manager.list_sessions(None).await;
823+
let status = |id: &str| {
824+
sessions[&infinity_protocol::ThreadRef::local(ThreadId::from_ref(id).to_owned())]
825+
.status
826+
.clone()
827+
};
828+
assert_eq!(status("killed"), SessionStatus::Idle);
829+
assert_eq!(status("stopped"), SessionStatus::Stopped);
830+
let persisted: serde_json::Value = serde_json::from_str(
831+
&std::fs::read_to_string(&sessions_path).expect("read sessions.json"),
832+
)
833+
.expect("parse sessions.json");
834+
assert_eq!(persisted["sessions"]["killed"]["idle"], true);
835+
})
836+
.await;
837+
}

‎crates/infinity-daemon/src/session_store.rs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,22 @@ impl SessionStore {
9797
Ok(())
9898
}
9999

100+
/// At cold start no drivers exist, so entries persisted as running
101+
/// (`!idle && !shut_down`, e.g. after a crash) are stale: mark them idle.
102+
/// Returns whether any entry changed. Does not `notify`, since no client
103+
/// is connected yet.
104+
pub fn reconcile_cold_start(&mut self) -> bool {
105+
let mut changed = false;
106+
for (id, entry) in &mut self.sessions {
107+
if !entry.idle && !entry.shut_down {
108+
tracing::info!("session {id} was persisted as running; marking it idle");
109+
entry.idle = true;
110+
changed = true;
111+
}
112+
}
113+
changed
114+
}
115+
100116
pub fn notify(&self, session_id: &ThreadId<str>) {
101117
if let Some(ref tx) = self.change_tx {
102118
let _ = tx.send(session_id.to_owned());

0 commit comments

Comments
 (0)