fix: restore codex idle detection and update codex and pi manifests

refs #4507
This commit is contained in:
Ogulcan Celik
2026-10-01 04:07:37 +03:00
parent 347f9c99bc
commit 07e3840bfb
7 changed files with 87 additions and 39 deletions
+28 -4
View File
@@ -1,7 +1,7 @@
id = "codex"
version = "2026.09.23.1"
version = "2026.10.01.1"
min_engine_version = 3
updated_at = "2026-09-23T22:12:00Z"
updated_at = "2026-10-01T00:00:00Z"
[[rules]]
id = "osc_title_blocked"
@@ -38,8 +38,20 @@ 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\?'] },
{ any = [
{ regex = ['\A> You are in [^\r\n]+(?:\r?\n|$)'] },
{ contains = ["Folder access"] },
] },
{ any = [
{ regex = ['(?s)Do\s+you\s+trust\s+the\s+contents\s+of\s+this\s+directory\?'] },
{ all = [
{ contains = ["Trust this folder?", "Codex can read, edit, and run files here"] },
{ any = [
{ contains = ["Trust and continue"] },
{ contains = ["enter continue"] },
] },
] },
] },
]
[[rules]]
@@ -92,3 +104,15 @@ any = [{ contains = [" to interrupt)"] }, { contains = ["s)"] }]
regex = ['(?m)^(?:[•◦][ \t]+)?[^\s›•◦■✗✓─][^\r\n]* \((?:[0-9]+[hm] )*[0-9]+s(?: • [^\r\n]+? 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+[^\r\n]+?\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']
# A failed reconnect keeps its final elapsed timer but is no longer working.
not = [{ line_regex = ['^(?:[•◦][ \t]+)?Reconnect failed — check the endpoint, then relaunch \([0-9hms ]+\)$'] }]
[[rules]]
id = "osc_title_idle"
state = "idle"
priority = 100
region = "osc_title"
visible_idle = true
regex = ['\S']
not = [
{ regex = ['(?:^| )[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏](?: |$)'] },
{ contains = ["Action Required"] },
]
+10 -4
View File
@@ -1,7 +1,7 @@
id = "pi"
version = "2026.09.14.1"
version = "2026.10.01.1"
min_engine_version = 1
updated_at = "2026-09-14T00:00:00Z"
updated_at = "2026-10-01T00:00:00Z"
aliases = ["herdr:pi"]
[[rules]]
@@ -10,7 +10,10 @@ state = "working"
priority = 100
region = "whole_recent"
visible_working = true
contains = ["Working..."]
any = [
{ contains = ["Working..."] },
{ line_regex = ['^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working$'] },
]
[[rules]]
id = "working_border"
@@ -18,4 +21,7 @@ state = "working"
priority = 100
region = "bottom_non_empty_lines(12)"
visible_working = true
line_regex = ['^── [⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working ─+$']
any = [
{ line_regex = ['^── [⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working ─+$'] },
{ line_regex = ['^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working$'] },
]
@@ -73,11 +73,9 @@ Some restricted Linux runtimes do not expose a terminal foreground process group
## Blocked state
Blocked detection is deliberately strict for screen-manifest agents. Herdr only marks `blocked` when the live bottom-buffer snapshot matches known visible approval, question, or permission UI. If no manifest rule matches for a known agent other than Codex, Herdr falls back to `idle` and labels that fallback as `default_known_agent_idle_fallback` in explain output. Codex falls back to `unknown` because its title and composer can look the same during an active turn and after a response.
Blocked detection is deliberately strict for screen-manifest agents. Herdr only marks `blocked` when the live bottom-buffer snapshot matches known visible approval, question, or permission UI. If no manifest rule matches for a known agent, Herdr falls back to `idle` and labels that fallback as `default_known_agent_idle_fallback` in explain output.
For those other agents, unusual new prompts may initially show as `idle` instead of `blocked` until Herdr learns that screen shape. The misclassification affects only the visible status and waits. It should not make Herdr send input or take destructive action.
For Codex, a visible spinner or live activity timer can establish `working`, and a visible approval prompt can establish `blocked`. An ordinary title, composer, or missing spinner cannot establish that a turn ended. Codex may therefore stay `unknown` after a response, and waits for `idle` or completion may time out. Managed startup uses the initial composer only to determine when it can accept a prompt; that observation does not change turn status.
This means unusual new agent prompts may initially show as `idle` instead of `blocked` until Herdr learns that screen shape. The misclassification affects only the visible status and waits. It should not make Herdr send input or take destructive action.
## Detection manifests
+3 -7
View File
@@ -540,14 +540,14 @@ fn fallback_explain(
)
})
.unwrap_or((None, Vec::new(), None, None, None, false));
let assume_idle = agent.is_some_and(|agent| agent != Agent::Codex);
let known_agent = agent.is_some();
let remote_update_status = include_update_status
.then(|| agent.and_then(remote_update_status))
.flatten();
DetectionExplain {
agent: agent.map(|agent| agent_label(agent).to_string()),
state: if assume_idle {
state: if known_agent {
AgentState::Idle
} else {
AgentState::Unknown
@@ -560,11 +560,7 @@ fn fallback_explain(
visible_working: false,
skip_state_update: false,
skipped_update_reason: None,
fallback_reason: match agent {
Some(Agent::Codex) => Some("codex_state_ambiguous".to_string()),
Some(_) => Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK.to_string()),
None => None,
},
fallback_reason: known_agent.then(|| DEFAULT_KNOWN_AGENT_IDLE_FALLBACK.to_string()),
evaluated_rules,
warning,
manifest_version,
+6 -12
View File
@@ -89,21 +89,15 @@ fn write_local_codex(content: &str) {
}
#[test]
fn codex_no_match_is_unknown_without_changing_other_agents() {
fn known_agent_no_match_defaults_to_idle_fallback() {
with_manifest_dirs("no-match", || {
write_local_codex(&local_manifest("working", "active-marker"));
let explain = explain(Agent::Codex, "unmatched-marker");
assert_eq!(explain.state, AgentState::Unknown);
assert_eq!(explain.state, AgentState::Idle);
assert!(!explain.visible_idle);
assert_eq!(
explain.fallback_reason.as_deref(),
Some("codex_state_ambiguous")
);
let other = fallback_explain(Some(Agent::Pi), None, false);
assert_eq!(other.state, AgentState::Idle);
assert_eq!(
other.fallback_reason.as_deref(),
Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK)
);
});
@@ -190,10 +184,10 @@ fn fallback_explain_preserves_active_manifest_version() {
let explain = explain(Agent::Codex, "ordinary prompt text");
assert_eq!(explain.state, AgentState::Unknown);
assert_eq!(explain.state, AgentState::Idle);
assert_eq!(
explain.fallback_reason.as_deref(),
Some("codex_state_ambiguous")
Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK)
);
assert_eq!(explain.manifest_version.as_deref(), Some("9999.01.01.1"));
assert!(matches!(
@@ -273,10 +267,10 @@ fn detection_uses_cached_manifest_until_explicit_reload() {
write_remote_codex_without_reload(&remote_manifest("9999.01.01.2", "working", "new-ready"));
let unchanged = explain(Agent::Codex, "new-ready");
assert_eq!(unchanged.state, AgentState::Unknown);
assert_eq!(unchanged.state, AgentState::Idle);
assert_eq!(
unchanged.fallback_reason.as_deref(),
Some("codex_state_ambiguous")
Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK)
);
assert_eq!(
unchanged.cached_remote_version.as_deref(),
+28 -4
View File
@@ -1,7 +1,7 @@
id = "codex"
version = "2026.09.23.1"
version = "2026.10.01.1"
min_engine_version = 3
updated_at = "2026-09-23T22:12:00Z"
updated_at = "2026-10-01T00:00:00Z"
[[rules]]
id = "osc_title_blocked"
@@ -38,8 +38,20 @@ 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\?'] },
{ any = [
{ regex = ['\A> You are in [^\r\n]+(?:\r?\n|$)'] },
{ contains = ["Folder access"] },
] },
{ any = [
{ regex = ['(?s)Do\s+you\s+trust\s+the\s+contents\s+of\s+this\s+directory\?'] },
{ all = [
{ contains = ["Trust this folder?", "Codex can read, edit, and run files here"] },
{ any = [
{ contains = ["Trust and continue"] },
{ contains = ["enter continue"] },
] },
] },
] },
]
[[rules]]
@@ -92,3 +104,15 @@ any = [{ contains = [" to interrupt)"] }, { contains = ["s)"] }]
regex = ['(?m)^(?:[•◦][ \t]+)?[^\s›•◦■✗✓─][^\r\n]* \((?:[0-9]+[hm] )*[0-9]+s(?: • [^\r\n]+? 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+[^\r\n]+?\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']
# A failed reconnect keeps its final elapsed timer but is no longer working.
not = [{ line_regex = ['^(?:[•◦][ \t]+)?Reconnect failed — check the endpoint, then relaunch \([0-9hms ]+\)$'] }]
[[rules]]
id = "osc_title_idle"
state = "idle"
priority = 100
region = "osc_title"
visible_idle = true
regex = ['\S']
not = [
{ regex = ['(?:^| )[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏](?: |$)'] },
{ contains = ["Action Required"] },
]
+10 -4
View File
@@ -1,7 +1,7 @@
id = "pi"
version = "2026.09.14.1"
version = "2026.10.01.1"
min_engine_version = 1
updated_at = "2026-09-14T00:00:00Z"
updated_at = "2026-10-01T00:00:00Z"
aliases = ["herdr:pi"]
[[rules]]
@@ -10,7 +10,10 @@ state = "working"
priority = 100
region = "whole_recent"
visible_working = true
contains = ["Working..."]
any = [
{ contains = ["Working..."] },
{ line_regex = ['^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working$'] },
]
[[rules]]
id = "working_border"
@@ -18,4 +21,7 @@ state = "working"
priority = 100
region = "bottom_non_empty_lines(12)"
visible_working = true
line_regex = ['^── [⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working ─+$']
any = [
{ line_regex = ['^── [⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working ─+$'] },
{ line_regex = ['^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working$'] },
]