Files
Eric YueandOgulcan Celik 7e51b2832d fix: detect codex activity with static titles and queued inputs (#4099)
* fix: detect codex activity with static titles and queued inputs

refs #4092

Codex 0.154.0 can omit the activity bullet when tui.animations is false
and replace the literal Working label with a task-specific label. Its
terminal title can also remain static during an active turn, so the OSC
spinner rule is not sufficient to identify work.

The screen fallback required a bullet followed by Working in the last
three non-empty lines. It therefore missed both the animation-disabled
status line and dynamic labels. Queued follow-ups add a heading, input
text, and an edit hint below the activity line, moving that line outside
the three-line region. The lower-priority non-empty OSC title rule then
reported idle while Codex still showed an active timer and interrupt
control.

Recognize the elapsed-time and esc-to-interrupt controls in the detection
snapshot before the current composer. Allow an optional bullet, a dynamic
activity label, and queued follow-ups below that line. Require that no
later response, completion, or interruption marker supersedes the live
activity. This excludes composer text and stale output without letting an
earlier interruption suppress a subsequent active turn.

Keep OSC state precedence and transcript-viewer skip behavior unchanged.
Update the bundled and distributed manifests together to 2026.09.14.1.
Add regression coverage for optional bullets, dynamic labels, queued
inputs, resumed work after interruption, and stale or quoted controls.

Avoid discarding Regex search caches on every detection poll. The loader
previously cloned the entire compiled rule vector, and each Regex clone
created a fresh search cache pool despite sharing compiled read-only
state. Share the immutable rules through Arc instead. Reload publishes a
new array while in-flight readers retain a valid old generation, without
holding the global cache lock during matching. Add a cheap contains gate
for the required interrupt text before running the multiline expression.

Add a deterministic regression for compiled-rule sharing, reload
replacement, retained old readers, and concurrent detection. Measure the
baseline and optimized detector kernels in three alternating rounds on
CPU 24 with NUMA node 0 and CPU 25 with NUMA node 1. Real queued detection
falls from about 116 to 22 microseconds, and two evaluations per pane over
15 panes fall from about 3.45 to 0.65 milliseconds on both sockets, about
81 percent less time. No benchmark match results change. The cache
improvement requires the updated binary, not just a manifest reload.

Validate on the official Herdr 0.9.0 release with Codex 0.154.0 by toggling
the old and new manifests during the same active queued task. Confirm
working for active turns and idle after completion or interruption.
Pass 86 captured-screen and semantic checks through the optimized source
binary, 69 manifest/cache tests, CPU-pinned render scaling, and the full
native CI suite: 3535 Rust tests passed, with six default skips. Run CI
serially with inherited Herdr session context removed; no tests were
excluded or expectations relaxed to make CI pass.

* fix: detect codex pending follow-ups as working

refs #4092

* fix: handle wrapped codex follow-up hints

refs #4092

---------

Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
2026-09-14 17:33:30 +03:00

104 lines
3.1 KiB
TOML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
id = "codex"
version = "2026.09.14.1"
min_engine_version = 3
updated_at = "2026-09-14T00:00:00Z"
[[rules]]
id = "osc_title_blocked"
state = "blocked"
priority = 1100
region = "osc_title"
visible_blocker = true
contains = ["Action Required"]
[[rules]]
id = "osc_title_working"
state = "working"
priority = 1050
region = "osc_title"
visible_working = true
regex = ['(?:^| )[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏](?: |$)']
[[rules]]
id = "transcript_viewer"
state = "unknown"
priority = 1000
region = "after_last_prompt_marker"
skip_state_update = true
contains = ["↑/↓ to scroll", "pgup/pgdn to", "home/end to jump", "q to quit"]
any = [
{ contains = ["esc to edit prev"] },
{ contains = ["esc/← to edit prev"] },
]
[[rules]]
id = "trust_directory"
state = "blocked"
priority = 950
region = "top_non_empty_lines(20)"
visible_blocker = true
all = [
{ regex = ['\A> You are in [^\r\n]+(?:\r?\n|$)'] },
{ regex = ['(?s)Do\s+you\s+trust\s+the\s+contents\s+of\s+this\s+directory\?'] },
]
[[rules]]
id = "startup_update"
state = "blocked"
priority = 950
region = "bottom_non_empty_lines(20)"
visible_blocker = true
contains = ["Update available!", "Update now"]
regex = ['Skip\s+until\s+next\s+version', 'Press enter to continue\s*\z']
[[rules]]
id = "live_strong_blocker"
state = "blocked"
priority = 900
region = "after_last_prompt_marker"
visible_blocker = true
any = [
{ contains = ["press enter to confirm or esc to cancel"] },
{ contains = ["enter to submit answer"] },
{ contains = ["enter to submit all"] },
{ contains = ["allow command?"] },
]
[[rules]]
id = "weak_blocker"
state = "blocked"
priority = 600
region = "whole_recent_without_current_prompt_marker"
# Sparkles can replace the space after . A later response marker makes that prompt stale.
not = [{ regex = ['(?m)^[⠁⠂⠄⠈⠐⠠⡀⢀][^\n]*(?:\n(?:[^•■✗✓\n][^\n]*)?)*\z'] }]
any = [
{ contains = ["[y/n]"] },
{ contains = ["yes (y)"] },
{ contains = ["do you want to"], any = [{ contains = ["yes"] }, { contains = [""] }] },
{ contains = ["would you like to"], any = [{ contains = ["yes"] }, { contains = [""] }] },
]
[[rules]]
id = "screen_working_fallback"
state = "working"
priority = 500
region = "before_current_prompt_marker"
visible_working = true
# Cheap negative filter before the multiline search.
contains = ["esc to interrupt"]
# Animations can hide the bullet, and queued inputs can sit below a dynamic activity label.
# Require the live timer/control suffix with no later response or interruption marker.
regex = ['(?m)^(?:[•◦][ \t]+)?[^\s›•◦■✗✓─][^\r\n]* \((?:[0-9]+[hm] )*[0-9]+s • esc to interrupt\)(?: · [^\r\n]*)?(?:\r?\n(?:[^•◦›■✗✓─\r\n][^\r\n]*|•[ \t]+(?:Queued\s+follow-up\s+inputs|Messages\s+to\s+be\s+submitted\s+after\s+next\s+tool\s+call(?:\s+\(press\s+esc\s+to\s+interrupt\s+and\s+send\s+immediately\))?|Messages\s+to\s+be\s+submitted\s+at\s+end\s+of\s+turn)|[⠁⠂⠄⠈⠐⠠⡀⢀][^\r\n]*)?)*\s*\z']
[[rules]]
id = "osc_title_idle"
state = "idle"
priority = 100
region = "osc_title"
visible_idle = true
regex = ['\S']
not = [
{ regex = ['(?:^| )[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏](?: |$)'] },
{ contains = ["Action Required"] },
]