Skip to content

Commit 4a8ae9c

Browse files
authored
Resume sessions on launcher restart (#620)
1 parent 1b62429 commit 4a8ae9c

4 files changed

Lines changed: 93 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 2.3.14
4+
5+
- Resume existing Claude sessions on launcher restart instead of creating new ones
6+
37
## 2.3.13
48

59
- Fix admin stats page crashing on non-401/403 error responses

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = ["shared", "backend", "frontend", "proxy", "claude-session-lib", "laun
33
resolver = "2"
44

55
[workspace.package]
6-
version = "2.3.13"
6+
version = "2.3.14"
77
edition = "2021"
88
authors = ["Matthew Goodman <d3a6d0cec0c16f3e@inboxnegative.com>"]
99

launcher/src/config.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use serde::{Deserialize, Serialize};
22
use shared::AgentType;
33
use std::path::PathBuf;
4+
use uuid::Uuid;
45

56
#[derive(Debug, Deserialize, Serialize, Default)]
67
pub struct LauncherConfig {
@@ -20,6 +21,8 @@ pub struct ExpectedSession {
2021
pub agent_type: AgentType,
2122
#[serde(default)]
2223
pub claude_args: Vec<String>,
24+
#[serde(default)]
25+
pub session_id: Option<Uuid>,
2326
}
2427

2528
fn config_path() -> PathBuf {
@@ -80,6 +83,24 @@ pub fn add_session(session: &ExpectedSession) -> anyhow::Result<()> {
8083
save_config(&config)
8184
}
8285

86+
pub fn update_session_id(working_directory: &str, session_id: Uuid) -> anyhow::Result<()> {
87+
let mut config = load_config();
88+
if let Some(session) = config
89+
.sessions
90+
.iter_mut()
91+
.find(|s| s.working_directory == working_directory)
92+
{
93+
session.session_id = Some(session_id);
94+
save_config(&config)?;
95+
tracing::debug!(
96+
"Updated session_id for {}: {}",
97+
working_directory,
98+
session_id
99+
);
100+
}
101+
Ok(())
102+
}
103+
83104
pub fn remove_session(working_directory: &str) -> anyhow::Result<()> {
84105
let mut config = load_config();
85106
let before = config.sessions.len();
@@ -147,6 +168,7 @@ auth_token = "secret"
147168
session_name: Some("my-session".to_string()),
148169
agent_type: AgentType::Claude,
149170
claude_args: vec!["--verbose".to_string()],
171+
session_id: None,
150172
}],
151173
};
152174
let serialized = toml::to_string_pretty(&config).unwrap();
@@ -186,10 +208,50 @@ claude_args = ["--model", "opus"]
186208
);
187209
assert_eq!(config.sessions[0].agent_type, AgentType::Claude);
188210
assert!(config.sessions[0].claude_args.is_empty());
211+
assert!(config.sessions[0].session_id.is_none());
189212

190213
assert_eq!(config.sessions[1].working_directory, "/home/user/project-b");
191214
assert!(config.sessions[1].session_name.is_none());
192215
assert_eq!(config.sessions[1].agent_type, AgentType::Codex);
193216
assert_eq!(config.sessions[1].claude_args, vec!["--model", "opus"]);
217+
assert!(config.sessions[1].session_id.is_none());
218+
}
219+
220+
#[test]
221+
fn parse_config_with_session_id() {
222+
let toml = r#"
223+
backend_url = "wss://example.com"
224+
auth_token = "tok_abc"
225+
226+
[[sessions]]
227+
working_directory = "/home/user/project-a"
228+
session_id = "550e8400-e29b-41d4-a716-446655440000"
229+
"#;
230+
let config: LauncherConfig = toml::from_str(toml).unwrap();
231+
assert_eq!(config.sessions.len(), 1);
232+
assert_eq!(
233+
config.sessions[0].session_id,
234+
Some(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap())
235+
);
236+
}
237+
238+
#[test]
239+
fn roundtrip_config_with_session_id() {
240+
let sid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
241+
let config = LauncherConfig {
242+
backend_url: None,
243+
auth_token: None,
244+
name: None,
245+
sessions: vec![ExpectedSession {
246+
working_directory: "/home/user/project".to_string(),
247+
session_name: None,
248+
agent_type: AgentType::Claude,
249+
claude_args: vec![],
250+
session_id: Some(sid),
251+
}],
252+
};
253+
let serialized = toml::to_string_pretty(&config).unwrap();
254+
let deserialized: LauncherConfig = toml::from_str(&serialized).unwrap();
255+
assert_eq!(deserialized.sessions[0].session_id, Some(sid));
194256
}
195257
}

launcher/src/connection.rs

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ async fn handle_message(
430430
agent_type,
431431
..
432432
} => {
433-
// Check if this is a scheduled launch
433+
// Check if this is a scheduled launch or a relaunch of an expected session
434434
let (resume_session_id, scheduled_task_id, is_scheduled) = if let Some((
435435
resume_id,
436436
task_id,
@@ -439,7 +439,11 @@ async fn handle_message(
439439
{
440440
(resume_id, Some(task_id), true)
441441
} else {
442-
(None, None, false)
442+
let resume_id = expected_sessions
443+
.iter()
444+
.find(|s| s.working_directory == working_directory)
445+
.and_then(|s| s.session_id);
446+
(resume_id, None, false)
443447
};
444448

445449
info!(
@@ -463,23 +467,28 @@ async fn handle_message(
463467
Ok(session_id) => {
464468
if is_scheduled {
465469
scheduler.on_session_spawned(request_id, session_id);
470+
} else if let Some(existing) = expected_sessions
471+
.iter_mut()
472+
.find(|s| s.working_directory == working_directory)
473+
{
474+
// Update stored session_id so future restarts resume this session
475+
existing.session_id = Some(session_id);
476+
if let Err(e) = config::update_session_id(&working_directory, session_id) {
477+
warn!("Failed to update session_id in config: {}", e);
478+
}
466479
} else {
467-
// Persist so this session survives launcher restarts
468-
if !expected_sessions
469-
.iter()
470-
.any(|s| s.working_directory == working_directory)
471-
{
472-
let expected = ExpectedSession {
473-
working_directory: working_directory.clone(),
474-
session_name: session_name.clone(),
475-
agent_type,
476-
claude_args: claude_args.clone(),
477-
};
478-
if let Err(e) = config::add_session(&expected) {
479-
warn!("Failed to persist session to config: {}", e);
480-
}
481-
expected_sessions.push(expected);
480+
// New session — persist so it survives launcher restarts
481+
let expected = ExpectedSession {
482+
working_directory: working_directory.clone(),
483+
session_name: session_name.clone(),
484+
agent_type,
485+
claude_args: claude_args.clone(),
486+
session_id: Some(session_id),
487+
};
488+
if let Err(e) = config::add_session(&expected) {
489+
warn!("Failed to persist session to config: {}", e);
482490
}
491+
expected_sessions.push(expected);
483492
}
484493
LauncherToServer::LaunchSessionResult {
485494
request_id,

0 commit comments

Comments
 (0)