Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
1d63b29
fix(tui): show Codex thread labels
senamakel Aug 8, 2026
2de2f83
chore: files changed src/sdk/src/session_history/summary.rs,patch_t4.py
senamakel Aug 8, 2026
b1ea6ae
fix(tui): refresh Codex thread label periodically after initial disco…
senamakel Aug 8, 2026
ddc4ae9
refactor(executor): extract session planning and launch into dedicate…
senamakel Aug 8, 2026
6181b1a
refactor(tui): expose executor internals for reuse
senamakel Aug 8, 2026
2c3ae00
test(tui): update OSC title clearing test for empty-title resilience
senamakel Aug 8, 2026
d595b1d
chore(tui): remove stale output files and fix thread name precedence …
senamakel Aug 9, 2026
34df1b7
fix(sdk): handle empty session history gracefully
senamakel Aug 9, 2026
0c69d77
chore(sdk): update session history summary wording
senamakel Aug 9, 2026
6ace8d5
fix(session_history): correct test assertion for empty history
senamakel Aug 9, 2026
e329b77
fix(session_history): correct test assertion for empty history
senamakel Aug 9, 2026
93541a0
fix(executor): handle turn completion when no pending tasks remain
senamakel Aug 9, 2026
4e78f53
fix(executor): restore turn state after worker restart
senamakel Aug 9, 2026
b793be8
Merge remote-tracking branch 'upstream/main' into pr/253
senamakel Aug 9, 2026
bdabf99
chore(deps): update openhuman subproject commit
senamakel Aug 9, 2026
e961344
feat(tui): add e2e test for codex rename functionality
senamakel Aug 9, 2026
7443aba
chore(deps): update openhuman subproject commit
senamakel Aug 9, 2026
682126f
fix(pty): handle screen resize events correctly
senamakel Aug 9, 2026
91eda05
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/253
senamakel Aug 9, 2026
ef85008
chore(sdk): remove unused session history summary module
senamakel Aug 9, 2026
43cfb92
chore(sdk): remove unused session history summary module
senamakel Aug 9, 2026
276fe15
fix(session_history): restore missing test assertions
senamakel Aug 9, 2026
7ece252
fix(session_history): restore missing test module
senamakel Aug 9, 2026
2a8e6f9
test(session_history): format assertion for readability
senamakel Aug 9, 2026
508e0d6
fix(executor): restore turn completion after worker restart
senamakel Aug 9, 2026
558a442
fix(session_history): handle empty scan results gracefully
senamakel Aug 9, 2026
d39ccdd
chore(sdk): add summary history persistence
senamakel Aug 9, 2026
ec678d7
fix(session_history): restore missing test assertions
senamakel Aug 9, 2026
2d66f7e
chore(tui): add pty session tests
senamakel Aug 9, 2026
59ed3a1
fix(session_history): restore missing test assertions
senamakel Aug 9, 2026
1f1e272
style: reformat idle timeout calculation and test command string
senamakel Aug 9, 2026
29619bc
fix(session_history): restore scan of removed history files
senamakel Aug 9, 2026
00570b7
fix(session_history): handle empty scan results gracefully
senamakel Aug 9, 2026
e7ca5b1
ci: re-trigger checks after label-attribution fix
senamakel Aug 9, 2026
0ed0b7f
ci: re-trigger workflow dispatch
senamakel Aug 9, 2026
15630f1
Merge remote-tracking branch 'upstream/main' into pr/253
senamakel Aug 9, 2026
a92212a
Merge remote-tracking branch 'upstream/main' into pr/253
senamakel Aug 9, 2026
75d97bb
fix(session_history): restore scan of history files
senamakel Aug 9, 2026
4e4e9bc
fix(session_history): restore scan of history files
senamakel Aug 9, 2026
a2372ec
chore(sdk): reformat session history scan closure
senamakel Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/sdk/src/session_history/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::collections::HashMap;
use super::scan::{
claude_sessions_dir, codex_sessions_dir, collect_session_files, is_here, safe_resolve,
};
use super::summary::read_session_summary;
use super::summary::{codex_thread_label, read_session_summary};
use super::types::{RawSessionFile, RecentSession, SessionAgentKind};

/// Default number of ranked sessions returned when no limit is given.
Expand Down Expand Up @@ -56,12 +56,17 @@ pub fn list_recent_sessions(
continue;
}
}
let label = if file.agent == SessionAgentKind::Codex {
codex_thread_label(env, &summary.id).unwrap_or(summary.label)
Comment thread
senamakel marked this conversation as resolved.
Outdated
} else {
summary.label
};
by_id.insert(
key,
RecentSession {
agent: file.agent,
id: summary.id,
label: summary.label,
label,
last_active: file.mtime_ms,
path: file.path.to_string_lossy().into_owned(),
cwd: summary.cwd,
Expand Down
1 change: 1 addition & 0 deletions src/sdk/src/session_history/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod tests;

pub use list::list_recent_sessions;
pub use scan::{claude_sessions_dir, codex_sessions_dir};
pub use summary::codex_thread_label;
pub use types::{RecentSession, SessionAgentKind};

pub(crate) use scan::{collect_session_files, discover_session_file, preexisting_session_files};
23 changes: 23 additions & 0 deletions src/sdk/src/session_history/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! the cost of scanning many transcripts. Claude and Codex use different record
//! shapes, so each has its own head reader.

use std::collections::HashMap;
use std::path::Path;

use serde_json::Value;
Expand Down Expand Up @@ -162,6 +163,28 @@ pub(super) fn slug_label(text: &str) -> String {
slug(text)
}

/// Read Codex's persisted name for `session_id`, when it has one.
///
/// Codex records `/rename` names in `session_index.jsonl` beside its `sessions`
/// directory rather than updating the terminal title. Claude has no equivalent
/// index, so callers use this only for Codex and retain the transcript-prompt
/// fallback when the index has not caught up yet.
pub fn codex_thread_label(env: &HashMap<String, String>, session_id: &str) -> Option<String> {
let index = super::scan::codex_sessions_dir(env)
.parent()?
.join("session_index.jsonl");
let contents = std::fs::read_to_string(index).ok()?;
contents.lines().find_map(|line| {
Comment thread
senamakel marked this conversation as resolved.
Outdated
let record: Value = serde_json::from_str(line).ok()?;
let object = record.as_object()?;
(object.get("id").and_then(Value::as_str) == Some(session_id))
.then(|| object.get("thread_name").and_then(Value::as_str))
.flatten()
.map(slug_label)
.filter(|label| !label.is_empty())
})
}

/// Read the first [`HEAD_BYTES`] of `path` as UTF-8 (lossy) and split into
/// non-empty lines, dropping a final partial line when the read hit the cap.
fn read_head_lines(path: &Path) -> Vec<String> {
Expand Down
38 changes: 33 additions & 5 deletions src/sdk/src/session_history/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

use super::scan::{collect_session_files, is_here, is_session_file, sessions_dir_for};
use super::summary::{
as_message_content, extract_text, first_prompt_text, read_claude_summary, read_codex_summary,
slug_label,
as_message_content, codex_thread_label, extract_text, first_prompt_text, read_claude_summary,
read_codex_summary, slug_label,
};
use super::*;
use crate::ui::util::SLUG_MAX_CHARS;
Expand All @@ -26,7 +26,7 @@ fn write_session(dir: &Path, name: &str, contents: &str) -> PathBuf {
fn ranks_current_cwd_first_then_recency() {
let tmp = std::env::temp_dir().join(format!("medulla-sh-{}", std::process::id()));
let claude_dir = tmp.join("claude");
let codex_dir = tmp.join("codex");
let codex_dir = tmp.join("codex").join("sessions");
fs::create_dir_all(&claude_dir).unwrap();
fs::create_dir_all(&codex_dir).unwrap();

Expand All @@ -53,6 +53,11 @@ fn ranks_current_cwd_first_then_recency() {
serde_json::json!({"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"do B here"}]}})
),
);
fs::write(
tmp.join("codex").join("session_index.jsonl"),
serde_json::json!({"id":"codex-b","thread_name":"Named Codex thread"}).to_string(),
)
.unwrap();

let mut env = HashMap::new();
env.insert(
Expand All @@ -69,8 +74,8 @@ fn ranks_current_cwd_first_then_recency() {
assert_eq!(sessions[0].id, "codex-b", "current-cwd session ranks first");
assert_eq!(sessions[0].agent, SessionAgentKind::Codex);
assert_eq!(
sessions[0].label, "b-here",
"the prompt is slugged, filler dropped"
sessions[0].label, "named-codex-thread",
"Codex's persisted thread name takes precedence over its prompt"
);
assert_eq!(sessions[1].id, "claude-a");
assert_eq!(sessions[1].label, "do-a");
Expand Down Expand Up @@ -254,6 +259,29 @@ fn codex_summary_uses_id_fallback_and_no_prompt_label() {
assert_eq!(summary.label, "(no prompt)");
}

#[test]
fn codex_thread_label_reads_the_persisted_rename() {
let home = tempfile::tempdir().unwrap();
let codex = home.path().join("codex");
fs::create_dir_all(codex.join("sessions")).unwrap();
fs::write(
codex.join("session_index.jsonl"),
serde_json::json!({"id":"codex-1","thread_name":"Ship the sidebar"}).to_string(),
)
.unwrap();
let mut env = HashMap::new();
env.insert(
"MEDULLA_CODEX_SESSIONS_DIR".to_string(),
codex.join("sessions").to_string_lossy().into_owned(),
);

assert_eq!(
codex_thread_label(&env, "codex-1").as_deref(),
Some("ship-sidebar")
);
assert_eq!(codex_thread_label(&env, "missing"), None);
}

#[test]
fn codex_summary_without_meta_is_none() {
let lines = vec![serde_json::json!({"type":"response_item"}).to_string()];
Expand Down
8 changes: 8 additions & 0 deletions src/tui/src/worker/executor/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,14 @@ impl PtySessionExecutor {
if let Some(located) = &poll.located {
Comment thread
senamakel marked this conversation as resolved.
Outdated
self.sessions
.record_session_id(id, located.harness_session_id.clone());
if provider == HarnessProvider::Codex {
if let Some(thread_name) = medulla::session_history::codex_thread_label(
&self.env,
&located.harness_session_id,
) {
Comment thread
senamakel marked this conversation as resolved.
Outdated
self.sessions.record_thread_name(id, thread_name);
}
}
}
for line in poll.lines {
*last_line_at = medulla::clock::now_millis();
Expand Down
9 changes: 9 additions & 0 deletions src/tui/src/worker/pty/handle/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ impl SessionHandle {
lock(&self.cold).last_error = Some(error);
}

/// Record the thread name discovered outside the terminal stream.
///
/// Codex persists renamed threads in its session index instead of emitting
/// an OSC window title, so the transcript executor supplies that value once
/// it has identified the session.
pub(in super::super) fn record_thread_name(&self, thread_name: String) {
lock(&self.cold).thread_name = Some(thread_name);
Comment thread
senamakel marked this conversation as resolved.
Outdated
}

/// The operator-facing projection of this session, for the list pane.
pub fn row(&self) -> SessionRow {
let cold = lock(&self.cold);
Expand Down
7 changes: 7 additions & 0 deletions src/tui/src/worker/pty/manager/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,13 @@ impl PtyManager {
}
}

/// Record a harness thread name learned from provider-owned session state.
pub(in crate::worker) fn record_thread_name(&self, id: &str, thread_name: String) {
if let Some(session) = self.handle(id) {
session.record_thread_name(thread_name);
}
}

/// Ask a session's harness to exit, then reap it.
///
/// Sends the child a kill rather than typing `/exit`: the harnesses disagree
Expand Down
Loading