diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/detect/manifest.rs | 14 | ||||
| -rw-r--r-- | src/detect/manifest/tests.rs | 125 | ||||
| -rw-r--r-- | src/detect/manifests/codex.toml | 13 |
3 files changed, 143 insertions, 9 deletions
diff --git a/src/detect/manifest.rs b/src/detect/manifest.rs index 1ca53228..acd6b8f9 100644 --- a/src/detect/manifest.rs +++ b/src/detect/manifest.rs @@ -1,6 +1,6 @@ use std::{ path::{Path, PathBuf}, - sync::{Mutex, OnceLock, RwLock}, + sync::{Arc, Mutex, OnceLock, RwLock}, }; use regex::Regex; @@ -123,7 +123,8 @@ pub struct RuleEvidence { #[derive(Debug, Clone)] struct LoadedManifest { manifest: AgentManifest, - compiled_rules: Vec<CompiledRule>, + // Keep Regex search caches warm across manifest loads and pane polling. + compiled_rules: Arc<[CompiledRule]>, source: ManifestSource, warning: Option<String>, cached_remote_version: Option<String>, @@ -453,7 +454,12 @@ fn evaluate_loaded_manifest( let mut matched: Option<(&ManifestRule, String)> = None; let mut evaluated_rules = Vec::new(); - for (rule, compiled_rule) in loaded.manifest.rules.iter().zip(&loaded.compiled_rules) { + for (rule, compiled_rule) in loaded + .manifest + .rules + .iter() + .zip(loaded.compiled_rules.iter()) + { let region_text = region(input, &rule.region); let matched_rule = compiled_rule_matches(compiled_rule, region_text); evaluated_rules.push(EvaluatedRule { @@ -703,7 +709,7 @@ fn loaded_manifest( cached_remote_version: Option<String>, local_override_shadowing_remote: bool, ) -> Result<LoadedManifest, String> { - let compiled_rules = compile_manifest(&manifest)?; + let compiled_rules = compile_manifest(&manifest)?.into(); Ok(LoadedManifest { manifest, compiled_rules, diff --git a/src/detect/manifest/tests.rs b/src/detect/manifest/tests.rs index c5522952..bdb0789d 100644 --- a/src/detect/manifest/tests.rs +++ b/src/detect/manifest/tests.rs @@ -290,6 +290,74 @@ fn detection_uses_cached_manifest_until_explicit_reload() { } #[test] +fn compiled_rules_are_shared_until_manifest_reload() { + with_manifest_dirs("shared-compiled-rules", || { + write_remote_codex(&format!( + "{}\nregex = ['^cached-[a-z]+$']\n", + remote_manifest("9999.01.01.1", "blocked", "cached-ready") + )); + let first = load_manifest(Agent::Codex).unwrap(); + let second = load_manifest(Agent::Codex).unwrap(); + assert!(!first.compiled_rules.is_empty()); + assert_eq!( + first.compiled_rules.as_ptr(), + second.compiled_rules.as_ptr(), + "cached loads must retain the same compiled rules and regex search caches" + ); + + write_remote_codex_without_reload(&format!( + "{}\nregex = ['^new-[a-z]+$']\n", + remote_manifest("9999.01.01.2", "working", "new-ready") + )); + let unchanged = load_manifest(Agent::Codex).unwrap(); + assert_eq!( + first.compiled_rules.as_ptr(), + unchanged.compiled_rules.as_ptr() + ); + + reload_manifests_for_agents(&[Agent::Codex]); + let reloaded = load_manifest(Agent::Codex).unwrap(); + let shared_reload = load_manifest(Agent::Codex).unwrap(); + assert_ne!( + first.compiled_rules.as_ptr(), + reloaded.compiled_rules.as_ptr() + ); + assert_eq!( + reloaded.compiled_rules.as_ptr(), + shared_reload.compiled_rules.as_ptr() + ); + assert!(compiled_rule_matches( + &first.compiled_rules[0], + "cached-ready" + )); + assert!(!compiled_rule_matches( + &first.compiled_rules[0], + "new-ready" + )); + assert_eq!( + explain(Agent::Codex, "new-ready").state, + AgentState::Working + ); + + std::thread::scope(|scope| { + for _ in 0..4 { + let reloaded = &reloaded; + scope.spawn(move || { + let loaded = load_manifest(Agent::Codex).unwrap(); + assert_eq!( + loaded.compiled_rules.as_ptr(), + reloaded.compiled_rules.as_ptr() + ); + for _ in 0..8 { + assert_eq!(detect(Agent::Codex, "new-ready").state, AgentState::Working); + } + }); + } + }); + }); +} + +#[test] fn all_bundled_manifests_parse_and_validate() { for agent in Agent::SCREEN_MANIFEST_AGENTS { assert!( @@ -1105,6 +1173,50 @@ fn codex_screen_working_fallback_handles_static_osc_title() { } #[test] +fn codex_screen_working_fallback_handles_activity_labels_and_queued_inputs() { + for prefix in ["", "• ", "◦ "] { + for label in ["Working", "Fixing bug in queue region"] { + for queue in [ + "", + "\n• Queued follow-up inputs\n ↳ Follow up after this turn\n alt + ↑ edit last queued message\n", + "\n• Messages to be submitted after next tool call\n (press esc to interrupt and send immediately)\n ↳ Keep waiting until the sleep finishes.\n", + "\n• Messages to be submitted after next tool call (press esc to interrupt and send immediately)\n ↳ Keep waiting until the sleep finishes.\n", + "\n• Messages to be submitted after next tool call (press esc to interrupt\n and send immediately)\n ↳ Keep waiting until the sleep finishes.\n", + "\n• Messages to be submitted after next\n tool call (press esc to interrupt and\n send immediately)\n ↳ Keep waiting until the sleep finishes.\n", + "\n• Messages to be submitted at end of turn\n ↳ Follow up after this turn\n", + "\n• Messages to be submitted after next tool call\n (press esc to interrupt and send immediately)\n ↳ Keep waiting until the sleep finishes.\n\n• Queued follow-up inputs\n ↳ After this turn reply ok.\n alt + ↑ edit last queued message\n", + ] { + let screen = format!( + "{prefix}{label} (1m 16s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close\n\ + {queue}\n› Ask Codex to do anything\n\n model · /work\n" + ); + let result = osc_explain(Agent::Codex, &screen, "project", ""); + + assert_eq!(result.state, AgentState::Working, "{screen}"); + assert_eq!( + result.matched_rule.as_ref().map(|rule| rule.id.as_str()), + Some("screen_working_fallback"), + "{screen}" + ); + assert!(result.visible_working); + } + } + } +} + +#[test] +fn codex_screen_working_fallback_uses_latest_activity_after_interruption() { + let screen = "■ Conversation interrupted\n\n\ + › Try again\n\n\ + Working (4s • esc to interrupt)\n\n\ + › Ask Codex to do anything\n\n model · /work\n"; + let result = osc_explain(Agent::Codex, screen, "project", ""); + + assert_eq!(result.state, AgentState::Working); + assert!(result.visible_working); +} + +#[test] fn codex_osc_working_remains_preferred_over_screen_fallback() { let screen = "• Working (4s • esc to interrupt)\n\n\ › Use /skills to list available skills\n\n\ @@ -1262,6 +1374,19 @@ fn codex_screen_working_fallback_ignores_stale_and_prompt_text() { " ◦ Working (1m 16s • esc to interrupt)\n\ › Use /skills to list available skills\n\ gpt-5.6-sol default · /work\n", + "Working (1m 16s • esc to interrupt)\n\ + • Finished the task\n\ + › Ask Codex to do anything\n", + "Working (1m 16s • esc to interrupt)\n\ + ■ Conversation interrupted\n\ + › Ask Codex to do anything\n", + "Working (1m 16s • esc to interrupt)\n\ + ─ Worked for 1m 16s ─\n\ + › Ask Codex to do anything\n", + "Working (1m 16s • esc to interrupt)\n•\nMessages to be submitted after next tool call\n› Ask Codex to do anything\n", + "› Explain this status:\n Working (1m 16s • esc to interrupt)\n", + "• Example (press esc to interrupt)\n\ + › Ask Codex to do anything\n", ]; for screen in screens { diff --git a/src/detect/manifests/codex.toml b/src/detect/manifests/codex.toml index 7840ca6f..5021567a 100644 --- a/src/detect/manifests/codex.toml +++ b/src/detect/manifests/codex.toml @@ -1,7 +1,7 @@ id = "codex" -version = "2026.09.12.1" +version = "2026.09.14.1" min_engine_version = 3 -updated_at = "2026-09-12T00:00:00Z" +updated_at = "2026-09-14T00:00:00Z" [[rules]] id = "osc_title_blocked" @@ -82,10 +82,13 @@ any = [ id = "screen_working_fallback" state = "working" priority = 500 -region = "bottom_non_empty_lines(3)" +region = "before_current_prompt_marker" visible_working = true -line_regex = ['^[•◦]\s+Working \([^)]*esc to interrupt\)(?: · .*)?$'] -not = [{ contains = ["■ Conversation interrupted"] }] +# 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" |
