mirror of
https://github.com/herdrdev/herdr.git
synced 2026-10-07 08:00:40 +00:00
fix: restore codex idle detection and update codex and pi manifests
refs #4507
This commit is contained in:
@@ -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"] },
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"] },
|
||||
]
|
||||
|
||||
@@ -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$'] },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user