Recover Codex sessions with missing rollouts
This commit is contained in:
1 parent
3af2502982
commit
46831520e3
3 files changed
+158
-9
No files matched your search
@@ -894,6 +894,41 @@ fn handle_response(inner: &Arc<Inner>, line: &Value) {
|
||||
if state.thread_request.as_deref() == Some(id) {
|
||||
state.thread_request = None;
|
||||
if let Some(error) = response_error(line) {
|
||||
// `thread/start` returns an id before the first turn creates its rollout. If the
|
||||
// app-server is restarted in that window, the id we correctly persisted is not one
|
||||
// Codex can resume. A rollout can also disappear independently of ai-app. In either
|
||||
// case our common transcript still exists, so start a blank Codex thread and mark the
|
||||
// point where the model's context stopped instead of stranding every queued message.
|
||||
if missing_thread(&error) && read_thread(&inner.session_dir).is_some() {
|
||||
protocol_error(inner, error.clone());
|
||||
let path = inner.session_dir.join(THREAD_FILE);
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(err) => {
|
||||
drop(state);
|
||||
protocol_error(
|
||||
inner,
|
||||
format!("couldn't forget the missing Codex thread: {err}"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let request = request_id();
|
||||
state.thread_request = Some(request.clone());
|
||||
drop(state);
|
||||
save_state(inner);
|
||||
send_json(
|
||||
inner,
|
||||
json!({
|
||||
"id": request,
|
||||
"method": "thread/start",
|
||||
"params": thread_params(inner)
|
||||
}),
|
||||
);
|
||||
let _ = inner.sink.send(Event::Cleared);
|
||||
return;
|
||||
}
|
||||
drop(state);
|
||||
protocol_error(inner, error);
|
||||
return;
|
||||
@@ -989,6 +1024,11 @@ fn response_error(line: &Value) -> Option<String> {
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
fn missing_thread(message: &str) -> bool {
|
||||
let message = message.to_ascii_lowercase();
|
||||
message.contains("thread not found") || message.contains("no rollout found for thread id")
|
||||
}
|
||||
|
||||
fn active_turn_not_steerable(line: &Value) -> bool {
|
||||
line.pointer("/error/data/codexErrorInfo/activeTurnNotSteerable")
|
||||
.is_some()
|
||||
@@ -1239,4 +1279,70 @@ mod tests {
|
||||
);
|
||||
assert_eq!(context_from_rollout(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_missing_thread_failures_are_recognised() {
|
||||
assert!(missing_thread(
|
||||
"thread not found: 01a099bd-2e0f-7442-b750-a9f33998c8c5"
|
||||
));
|
||||
assert!(missing_thread(
|
||||
"no rollout found for thread id 01a099bd-2e0f-7442-b750-a9f33998c8c5"
|
||||
));
|
||||
assert!(!missing_thread("thread is already running"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_thread_is_reported_then_replaced() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
write_thread(dir.path(), "missing-thread");
|
||||
let (sink, mut events) = mpsc::unbounded_channel();
|
||||
let (to_child, mut requests) = mpsc::unbounded_channel();
|
||||
let inner = Arc::new(Inner {
|
||||
sink,
|
||||
state: Mutex::new(ProtocolState {
|
||||
thread_request: Some("resume-request".to_string()),
|
||||
..ProtocolState::default()
|
||||
}),
|
||||
settings: Mutex::new(Settings {
|
||||
model: None,
|
||||
permission_mode: None,
|
||||
effort: None,
|
||||
}),
|
||||
to_child,
|
||||
transport: Transport::Here,
|
||||
session_dir: dir.path().to_path_buf(),
|
||||
subagents: Arc::new(Subagents::new(dir.path().to_path_buf())),
|
||||
reading: AtomicBool::new(true),
|
||||
});
|
||||
|
||||
handle_response(
|
||||
&inner,
|
||||
&json!({
|
||||
"id": "resume-request",
|
||||
"error": {"message": "thread not found: missing-thread"}
|
||||
}),
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
events.try_recv().expect("visible refusal"),
|
||||
Event::Error { message } if message ==
|
||||
"Codex refused a request: thread not found: missing-thread"
|
||||
));
|
||||
assert_eq!(events.try_recv().expect("context boundary"), Event::Cleared);
|
||||
assert_eq!(read_thread(dir.path()), None);
|
||||
let request: Value =
|
||||
serde_json::from_str(&requests.try_recv().expect("replacement thread request"))
|
||||
.expect("request json");
|
||||
assert_eq!(request["method"], "thread/start");
|
||||
let request_id = request["id"].as_str().expect("request id");
|
||||
|
||||
handle_response(
|
||||
&inner,
|
||||
&json!({"id": request_id, "result": {"thread": {"id": "replacement-thread"}}}),
|
||||
);
|
||||
assert_eq!(
|
||||
read_thread(dir.path()).as_deref(),
|
||||
Some("replacement-thread")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user