aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/detect/manifest.rs14
-rw-r--r--src/detect/manifest/tests.rs125
-rw-r--r--src/detect/manifests/codex.toml13
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"