fix: persist Codex resume sessions at launch (#3617)

refs #3517
This commit is contained in:
JJ Liebig
2026-09-04 14:54:05 +02:00
committed by GitHub
parent 3150bd92d6
commit 6045fe6a87
4 changed files with 103 additions and 4 deletions
+52
View File
@@ -69,6 +69,24 @@ pub fn session_ref_from_report(
agent_session_id.and_then(AgentSessionRef::id)
}
pub fn persisted_session_from_launch_args(
agent: crate::detect::Agent,
args: &[String],
) -> Option<PersistedAgentSession> {
let [command, session_id] = args else {
return None;
};
if agent != crate::detect::Agent::Codex || command != "resume" || session_id.starts_with('-') {
return None;
}
Some(PersistedAgentSession {
source: "herdr:codex".into(),
agent: "codex".into(),
session_ref: AgentSessionRef::id(session_id.clone())?,
})
}
pub fn normalize_session_start_source(value: Option<String>) -> Option<String> {
match value.as_deref().map(str::trim) {
Some(
@@ -282,6 +300,40 @@ mod tests {
));
}
#[test]
fn codex_noncanonical_resume_launch_has_no_explicit_session() {
assert_eq!(
persisted_session_from_launch_args(
crate::detect::Agent::Codex,
&["resume".into(), "codex-session".into()]
)
.unwrap()
.session_ref
.value,
"codex-session"
);
assert!(persisted_session_from_launch_args(
crate::detect::Agent::Codex,
&["resume".into(), "--last".into()]
)
.is_none());
assert!(persisted_session_from_launch_args(
crate::detect::Agent::Codex,
&["resume".into(), "not-a-session".into(), "--last".into()]
)
.is_none());
assert!(persisted_session_from_launch_args(
crate::detect::Agent::Codex,
&[
"--remote".into(),
"ws://example.test".into(),
"resume".into(),
"remote-session".into(),
]
)
.is_none());
}
#[test]
fn planner_allows_supported_agents() {
let pi_session = absolute_test_path("pi-session.jsonl");
+5
View File
@@ -160,6 +160,8 @@ impl App {
{
return Err(AgentStartError::InvalidArgument);
}
let persisted_agent_session =
crate::agent_resume::persisted_session_from_launch_args(kind, &params.args);
let conflicts = self.agent_name_conflicts(&name, "");
if !conflicts.is_empty() {
return Err(AgentStartError::DuplicateName {
@@ -217,6 +219,9 @@ impl App {
terminal.clear_agent_name();
return Err(AgentStartError::InputFailed(err.to_string()));
}
if let Some(session) = persisted_agent_session {
terminal.set_managed_agent_launch_session(session);
}
self.state.mark_session_dirty();
self.schedule_session_save();
+14 -2
View File
@@ -2782,9 +2782,9 @@ mod tests {
id: "req_agent_start_input".into(),
method: crate::api::schema::Method::AgentStart(crate::api::schema::AgentStartParams {
name: "worker".into(),
kind: "pi".into(),
kind: "codex".into(),
pane_id: pane_id.clone(),
args: Vec::new(),
args: vec!["resume".into(), "codex-session".into()],
timeout_ms: Some(4_000),
}),
};
@@ -2792,6 +2792,9 @@ mod tests {
let response: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(response["error"]["code"], "agent_start_input_failed");
assert_eq!(app.state.terminals[&terminal_id].agent_name, None);
assert!(app.state.terminals[&terminal_id]
.persisted_agent_session
.is_none());
assert_eq!(
app.state.terminals[&terminal_id].manual_label.as_deref(),
Some("shell")
@@ -2804,6 +2807,15 @@ mod tests {
let retry = app.handle_api_request(request());
let retry: serde_json::Value = serde_json::from_str(&retry).unwrap();
assert_eq!(retry["result"]["type"], "agent_started");
assert_eq!(
retry["result"]["agent"]["agent_session"],
serde_json::json!({
"source": "herdr:codex",
"agent": "codex",
"kind": "id",
"value": "codex-session",
})
);
assert_eq!(
app.state.terminals[&terminal_id].agent_name.as_deref(),
Some("worker")
+32 -2
View File
@@ -133,6 +133,7 @@ pub struct TerminalState {
pub agent_name: Option<String>,
agent_name_owner: Option<AgentNameOwner>,
managed_agent: Option<ManagedAgent>,
managed_agent_launch_session: Option<crate::agent_resume::PersistedAgentSession>,
hook_report_sequences: HashMap<String, u64>,
suppressed_full_lifecycle_hook_reports: HashMap<String, SuppressedFullLifecycleHookReport>,
stale_full_lifecycle_hook_sessions: HashMap<String, Vec<StaleFullLifecycleHookSession>>,
@@ -167,6 +168,7 @@ impl TerminalState {
agent_name: None,
agent_name_owner: None,
managed_agent: None,
managed_agent_launch_session: None,
hook_report_sequences: HashMap::new(),
suppressed_full_lifecycle_hook_reports: HashMap::new(),
stale_full_lifecycle_hook_sessions: HashMap::new(),
@@ -1367,6 +1369,14 @@ impl TerminalState {
self.persisted_agent_session = Some(session);
}
pub fn set_managed_agent_launch_session(
&mut self,
session: crate::agent_resume::PersistedAgentSession,
) {
self.persisted_agent_session = Some(session.clone());
self.managed_agent_launch_session = Some(session);
}
pub fn set_agent_session_ref(
&mut self,
source: String,
@@ -1587,11 +1597,15 @@ impl TerminalState {
self.hook_authority = None;
}
self.reconcile_agent_name_owner(&agent_label, Some(&session_ref));
self.persisted_agent_session = Some(crate::agent_resume::PersistedAgentSession {
let persisted_session = crate::agent_resume::PersistedAgentSession {
source,
agent: agent_label,
session_ref,
});
};
if self.managed_agent_launch_session.as_ref() == Some(&persisted_session) {
self.managed_agent_launch_session = None;
}
self.persisted_agent_session = Some(persisted_session);
let current_session = self.current_session_identity_for_persistence();
Some(TerminalStateMutation {
effective_state_change: self.recompute_effective_state(
@@ -1972,6 +1986,7 @@ impl TerminalState {
kind: managed.kind,
phase: ManagedAgentPhase::Active,
});
self.managed_agent_launch_session = None;
return true;
}
return false;
@@ -1999,6 +2014,7 @@ impl TerminalState {
kind: managed.kind,
phase: ManagedAgentPhase::Active,
});
self.managed_agent_launch_session = None;
return true;
}
if ready_after.is_some() {
@@ -2041,6 +2057,14 @@ impl TerminalState {
}
pub fn clear_agent_name(&mut self) {
if self
.managed_agent_launch_session
.take()
.as_ref()
.is_some_and(|session| self.persisted_agent_session.as_ref() == Some(session))
{
self.persisted_agent_session = None;
}
self.agent_name = None;
self.agent_name_owner = None;
self.managed_agent = None;
@@ -2269,9 +2293,15 @@ mod tests {
Duration::from_millis(10),
Duration::from_millis(20),
);
timed_out.set_managed_agent_launch_session(crate::agent_resume::PersistedAgentSession {
source: "herdr:codex".into(),
agent: "codex".into(),
session_ref: crate::agent_resume::AgentSessionRef::id("codex-session").unwrap(),
});
assert!(timed_out.reconcile_managed_agent_at(now + Duration::from_millis(20), false));
assert_eq!(timed_out.agent_name, None);
assert_eq!(timed_out.managed_agent_kind(), None);
assert!(timed_out.persisted_agent_session.is_none());
}
#[test]