Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Treat raw PTY captures as binary data.
tests/corpus/*.bin binary
tests/corpus/**/*.bin binary
4 changes: 4 additions & 0 deletions docs/agent-resume.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ After each turn, the notifier writes the `agent-turn-complete` JSON argument to

Replacing a configured notifier would change user behavior, so the harness reads bare top-level keys in `$CODEX_HOME/config.toml` until the first table header. If it finds a one-line `notify` array of non-empty basic strings, it chains that notifier after the capture write. `FLEETCOM_NOTIFY_CHAIN` carries its argv joined by newlines; the capture script appends the notification payload before invoking it. An absent setting or empty array means capture runs alone. This encoding cannot transport empty arguments, newlines, or NUL, so those values disable injection.

Live validation on 2026-09-28 used the official macOS arm64 Codex 0.157.1 and 0.158.0 executables with isolated homes and a scripted loopback Responses server. In both releases, the real capture script preserved the notification JSON and passed it to the configured notifier. Saving and loading through foreground fleetcom preserved the thread ID and reopened its history. These runs used `--no-daemon`; separate default-launch probes also captured and saved the ID, but their fresh homes had no managed Codex daemon installed or running. Shared-daemon behavior and real-account behavior remain unverified.

The [recorded terminal fixtures](../tests/corpus/codex_0158_terminal/README.md) preserve resumed-history and clipboard output from those runs. Offline replay checks the recorded display and OSC 52 forwarding; it does not run the notifier or execute save/load. The separate `tests/daemon_resume.rs` integration tests exercise session resume with stub agents.

### `grok`

You can specify a Grok ID at launch, but cannot inject a live-capture channel. For a bare command, `--session-id '<uuid>'` is added; no instrumentation is required for a canonical resume command.
Expand Down
82 changes: 81 additions & 1 deletion src/harness/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,17 +440,97 @@ fn codex_model_label(rows: &[String]) -> Option<String> {
if !rows[i].starts_with(' ') {
return None;
}
let segs: Vec<&str> = rows[i].trim().split(" · ").collect();
let row = rows[i].trim();
let footer = codex_footer_without_warning(row)?;
let segs: Vec<&str> = footer.split(" · ").collect();
if segs[0].is_empty() {
return None;
}
// The warning reduces footer width; an ellipsis in the model item is
// truncation, even when the remaining words satisfy the effort shape.
if footer != row && (segs[0].contains('…') || segs[0].contains("...")) {
return None;
}
let in_out = segs.len() >= 3
&& segs[segs.len() - 2].ends_with(" in")
&& segs[segs.len() - 1].ends_with(" out");
(in_out || codex_model_with_reasoning(segs[0])).then(|| segs[0].to_string())
})
}

/// The warning occupies a separate right-aligned area with at least two spaces
/// before it. Its middle dot belongs to the badge, not the model status line.
fn codex_footer_without_warning(row: &str) -> Option<&str> {
if !row.contains('⚠') {
return Some(row);
}
let (footer, badge) = row.rsplit_once(" ⚠ ")?;
let (count_label, hint) = badge
.split_once(" · ")
.map_or((badge, None), |(count, hint)| (count, Some(hint)));
let count_text = count_label.split(' ').next()?;
let count = count_text.parse::<usize>().ok()?;
if count == 0 || count.to_string() != count_text || footer.contains('⚠') {
return None;
}
if count_label != count_text {
let suffix = if count == 1 { " warning" } else { " warnings" };
if count_label.strip_prefix(count_text)? != suffix
|| !codex_warning_shortcut(hint?.strip_suffix(" to view")?)
{
return None;
}
} else if hint.is_some_and(|hint| !codex_warning_shortcut(hint)) {
return None;
}
Some(footer.trim_end())
}

/// Warning hints are one key, a two-key chord, or the unbound command fallback.
fn codex_warning_shortcut(hint: &str) -> bool {
if hint == "/warnings" {
return true;
}
let key_label = |mut key: &str| {
for modifier in ["ctrl+", "shift+"] {
key = key.strip_prefix(modifier).unwrap_or(key);
}
key = key
.strip_prefix("alt+")
.or_else(|| key.strip_prefix("⌥+"))
.unwrap_or(key);
(key.len() == 1 && !key.starts_with(char::is_whitespace))
|| matches!(
key,
"enter"
| "space"
| "tab"
| "esc"
| "backspace"
| "delete"
| "del"
| "fwd del"
| "home"
| "end"
| "pgup"
| "pgdn"
| "↑"
| "↓"
| "←"
| "→"
)
|| key.strip_prefix('f').is_some_and(|number| {
number
.parse::<u8>()
.is_ok_and(|n| (1..=24).contains(&n) && n.to_string() == number)
})
};
key_label(hint)
|| hint
.match_indices(' ')
.any(|(i, _)| key_label(&hint[..i]) && key_label(&hint[i + 1..]))
}

/// Whether an item has the accepted `model-with-reasoning` shape: two or three words,
/// with a recognized effort word second. The optional third word occupies the
/// service-tier position. Require a fixed effort word to limit false matches against
Expand Down
244 changes: 244 additions & 0 deletions src/harness/summary_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,82 @@ fn codex_label_reads_either_status_line_shape() {
);
}

#[test]
fn codex_label_separates_right_aligned_warning_badges() {
let label = |row: &str| CodexSummary.model_label(&rs(&["›", "", row]));
for badge in [
"⚠ 1 warning · f2 to view",
"⚠ 12 warnings · ctrl+w to view",
"⚠ 2 warnings · ctrl+x w to view",
"⚠ 1 warning · /warnings to view",
"⚠ 1 · f2",
"⚠ 2 · shift+←",
"⚠ 3 · ⌥+w",
"⚠ 4 · alt+w",
"⚠ 5 · ctrl+x ctrl+w",
"⚠ 6 · /warnings",
"⚠ 1 warning · ctrl+shift+⌥+f24 to view",
"⚠ 1 · fwd del",
"⚠ 1 · ctrl+x ctrl+fwd del",
"⚠ 1",
"⚠ 12",
] {
for (footer, want) in [
("gpt-5.4 high", "gpt-5.4 high"),
("gpt-5.4 xhigh fast", "gpt-5.4 xhigh fast"),
("gpt-5.4 high · /tmp/project", "gpt-5.4 high"),
("gpt-5.4 · 28.2K in · 78 out", "gpt-5.4"),
("gpt-5.4 high · 5.26K used · 0 in · 0 out", "gpt-5.4 high"),
] {
for padding in [" ", " "] {
let row = format!(" {footer}{padding}{badge}");
assert_eq!(label(&row), Some(want.to_string()), "{row:?}");
}
}
}
}

#[test]
fn codex_label_rejects_warning_prose_and_incomplete_footers() {
let label = |row: &str| CodexSummary.model_label(&rs(&["›", "", row]));
for row in [
" gpt-5.4 high ⚠",
" gpt-5.4 high ⚠ check your configuration",
" gpt-5.4 high ⚠ 1 warning · read documentation to view",
" gpt-5.4 high ⚠ 1 · read documentation",
" gpt-5.4 high ⚠ 1 warning · f2",
" gpt-5.4 high ⚠ 1 warning",
" gpt-5.4 high ⚠ 1 warnings · f2 to view",
" gpt-5.4 high ⚠ 2 warning · f2 to view",
" gpt-5.4 high ⚠ 0",
" gpt-5.4 high ⚠ 01",
" gpt-5.4 high ⚠ -1",
" gpt-5.4 high ⚠ 1 ·",
" gpt-5.4 high ⚠ 1 · f2 extra words",
" gpt-5.4 high ⚠ 1 · f25",
" gpt-5.4 high ⚠ 1 warning · f2 to vie…",
" gpt-5.4 high ⚠ 1 warning · f2 to view",
" gpt-5.4 high⚠ 1",
" /tmp/project · gpt-5.4 high ⚠ 1",
"gpt-5.4 high ⚠ 1",
" gpt-5.4 ⚠ 1",
" gpt-5.4 none ⚠ 1",
" gpt-5.4 very high fast ⚠ 1",
" gpt-5.4 h… ⚠ 1",
" gpt-5.4 high fa… ⚠ 1",
" gpt-5.4… high ⚠ 1",
" gpt-5.4 high... ⚠ 1",
" gpt-5.4 · 0 in · 0 ou… ⚠ 1",
" gpt-5.4… · 0 in · 0 out ⚠ 1",
] {
assert_eq!(label(row), None, "{row:?}");
}

let mut rows = rs(&[" gpt-5.4 high ⚠ 1"]);
rows.extend(rs(&["one", "two", "three", "four", "five", "six"]));
assert_eq!(CodexSummary.model_label(&rows), None);
}

/// The braille spinner and the blocked-on-user blink each canonicalize to a
/// single string; idle and foreign titles pass through.
#[test]
Expand Down Expand Up @@ -1393,6 +1469,174 @@ fn omp_ascii_box_glyphs_do_not_anchor() {

// ------------------------------------------------------- corpus replay --

/// Each offset is a cumulative PTY byte checkpoint, not a standalone repaint.
fn codex_0158_checkpoint(bytes: &[u8], offset: usize, alternate: bool) -> Vec<String> {
let mut emu = Emulator::new(40, 120, 2000);
emu.process(&bytes[..offset]);
assert_eq!(emu.alternate_screen(), alternate);
emu.live_rows()
}

#[test]
fn corpus_codex_0158_working_queue_and_completed_turns() {
for (bytes, offsets, queue_hint) in [
(
include_bytes!("../../tests/corpus/codex_0158_preview/0.157.1-inline.bin").as_slice(),
[12405, 15613, 17650, 18716],
" ⌥+↑ edit last queued message",
),
(
include_bytes!("../../tests/corpus/codex_0158_preview/0.158.0-inline.bin").as_slice(),
[11311, 14478, 16558, 17253],
" shift+← edit last queued message",
),
] {
for (state, offset) in offsets.into_iter().enumerate() {
let rows = codex_0158_checkpoint(bytes, offset, false);
let has = |text: &str| rows.iter().any(|row| row == text);
let queued = state == 1;
let draft = state == 2;
assert_eq!(has("• Queued follow-up inputs"), queued);
assert_eq!(has(" ↳ Queued phase zero message"), queued);
assert_eq!(has(queue_hint), queued);
assert_eq!(has("› Queued phase zero message"), draft);
assert_eq!(has("› Ask Codex to do anything"), !draft);
assert_eq!(
has("• PHASE0 COMPLETE. Deterministic local response."),
state == 3
);
assert_eq!(
CodexSummary.live_preview(&rows),
(state != 3).then(|| ("Working".to_string(), "codex:working")),
"checkpoint {offset}"
);
}
}
}

fn codex_warning_footer_prefixes_working_preview(bytes: &[u8], offset: usize) {
let rows = codex_0158_checkpoint(bytes, offset, false);
assert_eq!(
CodexSummary.model_label(&rows),
Some("phase0-fixture-model default".to_string()),
"checkpoint {offset}"
);
assert_eq!(
corpus(&bytes[..offset], &CodexSummary, 120),
anchor("phase0-fixture-model default · Working", "codex:working"),
"checkpoint {offset}"
);
}

#[test]
fn corpus_codex_0157_warning_footer_prefixes_working_preview() {
codex_warning_footer_prefixes_working_preview(
include_bytes!("../../tests/corpus/codex_0158_preview/0.157.1-inline.bin"),
12405,
);
}

#[test]
fn corpus_codex_0158_warning_footer_prefixes_working_preview() {
codex_warning_footer_prefixes_working_preview(
include_bytes!("../../tests/corpus/codex_0158_preview/0.158.0-inline.bin"),
11311,
);
}

#[test]
fn corpus_codex_0158_command_approvals() {
for (bytes, offset) in [
(
include_bytes!("../../tests/corpus/codex_0158_preview/0.157.1-fullscreen-approval.bin")
.as_slice(),
11965,
),
(
include_bytes!("../../tests/corpus/codex_0158_preview/0.158.0-fullscreen-approval.bin")
.as_slice(),
12967,
),
] {
let rows = codex_0158_checkpoint(bytes, offset, true);
for text in [
" Would you like to run the following command?",
" $ printf 'PHASE0 APPROVAL\\n'",
"› 1. Yes, proceed (y)",
" 3. No, and tell Codex what to do differently (esc)",
] {
assert!(rows.iter().any(|row| row == text), "{text}");
}
assert_eq!(
corpus(&bytes[..offset], &CodexSummary, 120),
anchor("awaiting approval", "codex:approval-menu")
);
}
}

#[test]
fn corpus_codex_0158_stdin_requires_a_second_approval() {
for (bytes, command, input, needs_approval) in [
(
include_bytes!("../../tests/corpus/codex_0158_preview/0.157.1-fullscreen-stdin.bin")
.as_slice(),
12494,
16518,
false,
),
(
include_bytes!("../../tests/corpus/codex_0158_preview/0.158.0-fullscreen-stdin.bin")
.as_slice(),
13496,
17417,
true,
),
] {
let rows = codex_0158_checkpoint(bytes, command, true);
assert!(
rows.iter()
.any(|row| row == " Would you like to run the following command?")
);
assert_eq!(
CodexSummary.live_preview(&rows),
Some(("awaiting approval".to_string(), "codex:approval-menu"))
);
let rows = codex_0158_checkpoint(bytes, input, true);
assert!(
rows.iter()
.any(|row| row.starts_with("✔ You approved codex to run read line;"))
);
assert_eq!(
rows.iter()
.any(|row| row.starts_with(" Would you like to send input to terminal ")),
needs_approval
);
assert_eq!(
rows.iter().any(|row| row == " Input: \"fixture\\n\""),
needs_approval
);
assert_eq!(
rows.iter()
.any(|row| row == "• PHASE0 COMPLETE. Deterministic local response."),
!needs_approval
);
assert_eq!(
rows.iter().any(|row| row == " PHASE0 INPUT: fixture"),
!needs_approval
);
assert_eq!(
CodexSummary.live_preview(&rows),
needs_approval.then(|| ("awaiting approval".to_string(), "codex:approval-menu"))
);
if needs_approval {
assert_eq!(
corpus(&bytes[..input], &CodexSummary, 120),
anchor("awaiting approval", "codex:approval-menu")
);
}
}
}

/// Positive per-state fixtures at capture geometry (40×120): exact
/// normalized text, Anchor provenance, and the matcher id.
#[test]
Expand Down
Loading
Loading