Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 commits
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
2 changes: 1 addition & 1 deletion src/sdk/src/harness_hooks/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ fn every_spawn_seam_uses_the_merged_launch_builder() {
let seams = [
"src/sdk/src/wrapper/run/mod.rs",
"src/sdk/src/daemon/providers/execute.rs",
"src/tui/src/worker/executor/run.rs",
"src/tui/src/worker/executor/launch.rs",
"src/tui/src/ui/harness_pane/spawn.rs",
];
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
Expand Down
16 changes: 14 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_index_map, read_session_summary};
use super::types::{RawSessionFile, RecentSession, SessionAgentKind};

/// Default number of ranked sessions returned when no limit is given.
Expand Down Expand Up @@ -43,6 +43,10 @@ pub fn list_recent_sessions(
raw.truncate(scan_limit);

let here = safe_resolve(cwd);
// Load the Codex session index once into an id-to-label map so
// every session below does not re-parse the index file on each call.
let codex_labels = codex_index_map(env);

// Dedupe by agent+id, keeping the freshest file.
let mut by_id: HashMap<String, RecentSession> = HashMap::new();
for file in &raw {
Expand All @@ -56,12 +60,20 @@ pub fn list_recent_sessions(
continue;
}
}
let label = if file.agent == SessionAgentKind::Codex {
codex_labels
.get(&summary.id)
.cloned()
.unwrap_or(summary.label)
} 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, codex_thread_label_for_cwd};
pub use types::{RecentSession, SessionAgentKind};

pub(crate) use scan::{collect_session_files, discover_session_file, preexisting_session_files};
33 changes: 33 additions & 0 deletions src/sdk/src/session_history/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,39 @@ pub(crate) fn discover_session_file(
None
}

/// Every session file for `agent` rooted at `cwd`, newest first.
///
/// [`discover_session_file`] returns the single newest match — right for
/// binding a tailer to "the session in this folder". This is the
/// attribution-shaped variant: it returns *all* matches so a caller can prove
/// the folder maps to exactly one session before trusting the newest's name.
///
/// Unlike `discover_session_file`, a transcript with no recorded cwd is not a
/// candidate: without one it cannot be shown to belong to this folder, so it
/// cannot anchor a label either.
pub(crate) fn session_files_for_cwd(
env: &HashMap<String, String>,
agent: SessionAgentKind,
cwd: &str,
) -> Vec<DiscoveredSession> {
let here = safe_resolve(cwd);
let mut files = collect_session_files(agent, &sessions_dir_for(env, agent));
files.sort_by_key(|file| std::cmp::Reverse(file.mtime_ms));
files
.into_iter()
.filter_map(|file| {
let canonical = std::fs::canonicalize(&file.path).unwrap_or_else(|_| file.path.clone());
let summary = read_session_summary(agent, &file.path)?;
let session_cwd = summary.cwd?;
(safe_resolve(&session_cwd) == here).then_some(DiscoveredSession {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
path: canonical,
id: summary.id,
cwd: Some(session_cwd),
})
})
.collect()
}

/// Whether a session's recorded `cwd` resolves to the same path as `here`.
/// Both sides must be present for a match.
pub(super) fn is_here(cwd: Option<&str>, here: Option<&str>) -> bool {
Expand Down
82 changes: 82 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,87 @@ pub(super) fn slug_label(text: &str) -> String {
slug(text)
}

/// Read Codex's persisted name for the session rooted at `cwd` — but only when
/// the folder is unambiguous about which session it belongs to.
///
/// The id-keyed [`codex_thread_label`] is the right read once a transcript has
/// been located, because identity beats recency. This is the fallback for a
/// session nothing has located yet — one an operator created and typed into
/// directly, which never enters the transcript executor — where the best
/// identity on offer is "the Codex rollout in this working directory".
///
/// The name is attributed only when exactly one rollout is rooted here. With
/// several — two sessions sharing a directory, or a stale rollout from a
/// finished one still newer — the cwd cannot prove which is this session's, and
/// answering with the newest would put another session's name on this row. The
/// fallback then declines and the row keeps its terminal-derived name until
/// identity is found.
pub fn codex_thread_label_for_cwd(env: &HashMap<String, String>, cwd: &str) -> Option<String> {
// `session_files_for_cwd` is cwd-strict (a transcript with no recorded cwd
// is not a candidate), so a single hit is a positive attribution, not a
// guess.
let mut candidates = super::scan::session_files_for_cwd(env, SessionAgentKind::Codex, cwd);
if candidates.len() != 1 {
return None;
}
let discovered = candidates.pop().expect("exactly one candidate");
codex_thread_label(env, &discovered.id)
}

/// 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.
///
/// The lookup goes through [`codex_index_map`] so this single-session read and
/// the batch loader pick the same winning record when the index holds several
/// entries for an id — the newest, last one — instead of the single read
/// disagreeing with the recent-session list.
pub fn codex_thread_label(env: &HashMap<String, String>, session_id: &str) -> Option<String> {
codex_index_map(env).get(session_id).cloned()
}

/// Load the Codex session-index into an id-to-label map.
///
/// Callers that need to resolve thread names for many sessions (e.g. the
/// recent-session list) can load the index once and look up every session
/// against the same map, rather than re-reading and re-parsing the file
/// per session.
///
/// `session_index.jsonl` is append-only: each `/rename` writes a new record for
/// the same id, so when several records share an id the last one — the newest
/// rename — wins. [`codex_thread_label`] routes through this map for the same
/// guarantee.
pub fn codex_index_map(env: &HashMap<String, String>) -> HashMap<String, String> {
let Some(index_path) = (|| {
super::scan::codex_sessions_dir(env)
.parent()?
.join("session_index.jsonl")
.into()
})() else {
return HashMap::new();
};
let Ok(contents) = std::fs::read_to_string(index_path) else {
return HashMap::new();
};
contents
.lines()
.filter_map(|line| {
let record: Value = serde_json::from_str(line).ok()?;
let object = record.as_object()?;
let id = object.get("id").and_then(Value::as_str)?;
let label = object
.get("thread_name")
.and_then(Value::as_str)
.map(slug_label)
.filter(|l| !l.is_empty())?;
Some((id.to_string(), label))
})
.collect()
}

/// 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
Loading