Recover Codex sessions with missing rollouts

This commit is contained in:
iris-ai committed 2026-09-14 15:01:47 -04:00
1 parent 3af2502982
commit 46831520e3
3 files changed
+158 -9

No files matched your search

+39 -9
View File
@@ -42,12 +42,13 @@ impl Translator {
if line.get("id").is_some() && line.get("method").is_none() {
return true;
}
if line.get("method").and_then(Value::as_str) == Some("thread/started")
&& line
.pointer("/params/thread/parentThreadId")
.is_some_and(|parent| !parent.is_null())
{
return false;
let kind = line.get("method").and_then(Value::as_str);
let body = kind.and_then(|_| line.get("params")).unwrap_or(line);
if let Some(root) = started_thread_is_root(kind, body) {
// A clear or missing-rollout recovery replaces the root id inside this same
// app-server. The new root cannot match the id this translator still holds; its null
// parent is the authoritative distinction from a newly spawned subagent.
return root;
}
match (self.thread_id.as_deref(), notification_thread(line)) {
(Some(parent), Some(thread)) => parent == thread,
@@ -80,10 +81,13 @@ impl Translator {
self.remember_spawned_thread(kind, body);
let thread = notification_thread(line);
let root_started = started_thread_is_root(kind, body) == Some(true);
let child = thread.filter(|thread| {
self.thread_id
.as_deref()
.is_some_and(|parent| parent != *thread)
!root_started
&& self
.thread_id
.as_deref()
.is_some_and(|parent| parent != *thread)
});
if let Some(id) = child {
self.ensure_child(id, "subagent", None);
@@ -467,6 +471,13 @@ fn subagent_title(path: &str) -> String {
.replace('_', " ")
}
fn started_thread_is_root(kind: Option<&str>, body: &Value) -> Option<bool> {
(kind == Some("thread/started")).then(|| {
body.pointer("/thread/parentThreadId")
.is_none_or(Value::is_null)
})
}
fn start_item(item: &Value) -> Vec<Event> {
if matches!(
item.get("type").and_then(Value::as_str),
@@ -935,6 +946,25 @@ mod tests {
assert!(translator.limited());
}
#[test]
fn a_replacement_root_is_not_mistaken_for_a_subagent() {
let mut translator = Translator {
thread_id: Some("old-root".to_string()),
..Translator::default()
};
let replacement = line(
r#"{"method":"thread/started","params":{"thread":{"id":"new-root","parentThreadId":null}}}"#,
);
assert!(translator.is_parent(&replacement));
assert!(translator.translate(&replacement).is_empty());
assert_eq!(translator.thread_id.as_deref(), Some("new-root"));
let child = line(
r#"{"method":"thread/started","params":{"thread":{"id":"child","parentThreadId":"new-root"}}}"#,
);
assert!(!translator.is_parent(&child));
}
#[test]
fn command_translation_only_hides_the_known_bash_wrapper() {
let legacy = tool(&line(