From 394c92bbae7ed1f78dc58c7c922d4d7d0bbdb294 Mon Sep 17 00:00:00 2001
From: dawn <93917549+dawNotPoi@users.noreply.github.com>
Date: Mon, 21 Sep 2026 09:07:53 +0800
Subject: [PATCH 1/2] fix(codex): ignore pathless AGENTS instructions
---
src-tauri/src/parsers/codex.rs | 42 +++++++++++++++++++++++++++++++---
1 file changed, 39 insertions(+), 3 deletions(-)
diff --git a/src-tauri/src/parsers/codex.rs b/src-tauri/src/parsers/codex.rs
index eedb7f08cd..eaa1c8be4a 100644
--- a/src-tauri/src/parsers/codex.rs
+++ b/src-tauri/src/parsers/codex.rs
@@ -5344,9 +5344,16 @@ fn strip_agents_instructions_block(input: &str) -> String {
}
fn is_agents_instruction_message(input: &str) -> bool {
- input
- .trim_start()
- .starts_with("# AGENTS.md instructions for ")
+ const HEADER: &str = "# AGENTS.md instructions";
+
+ let Some(suffix) = input.trim_start().strip_prefix(HEADER) else {
+ return false;
+ };
+
+ suffix.is_empty()
+ || suffix.starts_with('\n')
+ || suffix.starts_with("\r\n")
+ || suffix.starts_with(" for ")
}
fn is_environment_context_message(input: &str) -> bool {
@@ -6883,6 +6890,13 @@ mod tests {
assert!(got.is_none());
}
+ #[test]
+ fn skips_pathless_agents_instructions_title_candidate() {
+ let input = "# AGENTS.md instructions\n\n\nhello\n";
+ let got = extract_codex_title_candidate(input, true);
+ assert!(got.is_none());
+ }
+
#[test]
fn skips_environment_context_title_candidate() {
let input = "\n /tmp/demo\n";
@@ -13258,6 +13272,28 @@ mod tests {
let mut lines = String::from(
"{\"timestamp\":\"2026-03-01T10:00:00Z\",\"type\":\"session_meta\",\"payload\":{\"id\":\"deny-1\",\"cwd\":\"/tmp/demo\"}}\n",
);
+ lines.push_str(
+ &serde_json::json!({
+ "timestamp": "2026-03-01T10:00:01Z",
+ "type": "response_item",
+ "payload": {
+ "type": "message",
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "# AGENTS.md instructions\n\n\nhi\n"
+ },
+ {
+ "type": "input_text",
+ "text": "\n /tmp/demo\n"
+ }
+ ]
+ }
+ })
+ .to_string(),
+ );
+ lines.push('\n');
for (index, text) in [
"# AGENTS.md instructions for /tmp/demo\n\n\nhi\n",
"\n /tmp/demo\n",
From 071b451a80fecfe54c9ecf0c730e7562481c5171 Mon Sep 17 00:00:00 2001
From: xintaofei
Date: Mon, 21 Sep 2026 20:33:25 +0800
Subject: [PATCH 2/2] fix(codex): ignore the recommended-plugins fragment,
tighten the AGENTS matcher
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`` is one more `role: "user"` context fragment newer codex
injects ahead of the first prompt, so it won the promoted-title race and named
whole fleets of sessions after it. Deny it alongside the other injected
envelopes.
Require the `` opener before treating a `# AGENTS.md instructions`
header as machinery: the pathless header is one newline away from an ordinary
Markdown H1, and a prompt that merely opens with that heading was being dropped
from the transcript entirely, not just passed over for the title.
Cover both parsers end to end — the injection precedes the prompt, so the
regression is only visible once a real prompt follows it.
---
src-tauri/src/parsers/codex.rs | 167 +++++++++++++++++++++++++++------
1 file changed, 138 insertions(+), 29 deletions(-)
diff --git a/src-tauri/src/parsers/codex.rs b/src-tauri/src/parsers/codex.rs
index eaa1c8be4a..b944de48d8 100644
--- a/src-tauri/src/parsers/codex.rs
+++ b/src-tauri/src/parsers/codex.rs
@@ -5343,6 +5343,23 @@ fn strip_agents_instructions_block(input: &str) -> String {
text.trim().to_string()
}
+/// Whether `input` is the AGENTS.md context codex injects as a `role: "user"`
+/// record — in either of the two shapes it ships in.
+///
+/// Upstream (`UserInstructions` in
+/// `codex-rs/core/src/context/user_instructions.rs`) renders it as the header,
+/// then `{" for {dir}"|""}`, then `\n\n\n{text}\n`, then the
+/// closing tag. The directory is an `Option`, and a GLOBAL `~/.codex/AGENTS.md`
+/// has no directory to name — so newer codex emits the header bare, which used
+/// to slip past the old `"…instructions for "` prefix and become the
+/// conversation title (#789).
+///
+/// The `` opener is unconditional in both shapes (2554 of 2554
+/// such records in the local corpus carry it), so requiring it costs nothing
+/// and keeps a human prompt that merely OPENS with this heading from being
+/// swallowed as machinery. The record often carries ``
+/// as a second content item, joined onto the same text, which is why the
+/// closing tag is deliberately NOT anchored to the end.
fn is_agents_instruction_message(input: &str) -> bool {
const HEADER: &str = "# AGENTS.md instructions";
@@ -5350,10 +5367,9 @@ fn is_agents_instruction_message(input: &str) -> bool {
return false;
};
- suffix.is_empty()
- || suffix.starts_with('\n')
- || suffix.starts_with("\r\n")
- || suffix.starts_with(" for ")
+ // `\r` covers the CRLF rollouts Windows codex writes.
+ (suffix.starts_with('\n') || suffix.starts_with('\r') || suffix.starts_with(" for "))
+ && suffix.contains("")
}
fn is_environment_context_message(input: &str) -> bool {
@@ -5383,6 +5399,16 @@ fn is_codex_internal_context_message(input: &str) -> bool {
/// `exec_command` (34 occurrences) — matching English prose is admittedly
/// brittle, but the failure mode is one stray user bubble in a rollout that
/// would otherwise render nothing at all.
+///
+/// `` is the newest member and was NOT in that census —
+/// codex only writes it once the `recommended_plugins` feature is on, which no
+/// local rollout had. It is upstream's `RecommendedPluginsInstructions`
+/// (`codex-rs/core/src/context/recommended_plugins_instructions.rs`), a
+/// `role: "user"` fragment rendered as the marker pair wrapped around
+/// `"\nHere is a list of plugins that are available but not installed.\n\n…"`.
+/// Because codex injects it BEFORE the first prompt it does not merely add a
+/// stray bubble: it wins the promoted-title race, which is how whole fleets of
+/// sessions ended up named ` Here is a list of plugins…`.
const PROMOTED_USER_DENY_PREFIXES: &[&str] = &[
"",
"",
"Warning: apply_patch was requested via exec_command",
];
@@ -6424,6 +6451,7 @@ mod tests {
use super::BudgetedSink;
use super::MCP_RESULT_FALLBACK_CAP;
use super::is_encrypted_envelope;
+ use super::is_promotable_user_text;
use super::merge_codex_context_window_stats;
use super::native_team_wait_input;
use super::merge_codex_total_usage_stats;
@@ -6895,6 +6923,25 @@ mod tests {
let input = "# AGENTS.md instructions\n\n\nhello\n";
let got = extract_codex_title_candidate(input, true);
assert!(got.is_none());
+
+ // Windows codex writes the same record with CRLF.
+ let crlf = "# AGENTS.md instructions\r\n\r\n\r\nhello\r\n";
+ assert!(extract_codex_title_candidate(crlf, true).is_none());
+ }
+
+ #[test]
+ fn keeps_a_human_prompt_that_merely_opens_with_the_agents_heading() {
+ // The pathless header is one newline away from an ordinary Markdown H1,
+ // so the envelope's `` body is what separates codex's
+ // injection from a person asking about it. Without that second signal
+ // this prompt is dropped from the transcript entirely — not merely
+ // passed over for the title.
+ let input = "# AGENTS.md instructions\n\n为什么我的全局 AGENTS.md 没有生效?";
+ assert_eq!(
+ extract_codex_title_candidate(input, true).as_deref(),
+ Some("# AGENTS.md instructions\n\n为什么我的全局 AGENTS.md 没有生效?")
+ );
+ assert!(is_promotable_user_text(input));
}
#[test]
@@ -7212,36 +7259,97 @@ mod tests {
));
}
+ /// One `response_item` user record carrying `texts` as its content items.
+ fn injected_user_record(ts: &str, texts: &[&str]) -> String {
+ format!(
+ "{}\n",
+ serde_json::json!({
+ "timestamp": ts,
+ "type": "response_item",
+ "payload": {
+ "type": "message",
+ "role": "user",
+ "content": texts
+ .iter()
+ .map(|text| serde_json::json!({"type": "input_text", "text": text}))
+ .collect::>(),
+ }
+ })
+ )
+ }
+
#[test]
fn summary_title_skips_injected_messages_and_uses_real_prompt() {
- let nanos = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("system time ok")
- .as_nanos();
- let path: PathBuf = env::temp_dir().join(format!("codeg-codex-test-{nanos}.jsonl"));
-
// Injected/duplicate context arrives as text-only `response_item` user
- // messages (AGENTS.md, environment_context); the real prompt is delivered
- // by `event_msg.user_message` — the canonical prompt channel in real codex
- // rollouts. The summary titles from the real prompt and, like the detail
- // parser, never from a text-only `response_item` (which detail does not
- // render as a user turn at all).
- let content = concat!(
- "{\"timestamp\":\"2026-03-01T10:00:00Z\",\"type\":\"session_meta\",\"payload\":{\"id\":\"test-1\",\"cwd\":\"/tmp/demo\"}}\n",
- "{\"timestamp\":\"2026-03-01T10:00:01Z\",\"type\":\"response_item\",\"payload\":{\"type\":\"message\",\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"# AGENTS.md instructions for /tmp/demo\\n\\n\\nhello\\n\"}]}}\n",
- "{\"timestamp\":\"2026-03-01T10:00:02Z\",\"type\":\"response_item\",\"payload\":{\"type\":\"message\",\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"\\n /tmp/demo\\n\"}]}}\n",
- "{\"timestamp\":\"2026-03-01T10:00:03Z\",\"type\":\"event_msg\",\"payload\":{\"type\":\"user_message\",\"message\":\"真实用户标题\"}}\n"
- );
- fs::write(&path, content).expect("write test jsonl");
+ // messages; the real prompt is delivered by `event_msg.user_message` —
+ // the canonical prompt channel in real codex rollouts. Both parsers must
+ // title from the prompt and never from the injection, which is the whole
+ // point: the injection PRECEDES the prompt, so anything that survives the
+ // deny-lists outranks it in stream order and wins the title.
+ //
+ // All three injected shapes are real. The AGENTS.md header ships with or
+ // without a path — `UserInstructions { directory: Option }`
+ // upstream, and a global `~/.codex/AGENTS.md` has no directory to name
+ // (#789) — and always rides in the same record as ``
+ // (1857 of 2033 such records in the local corpus). ``
+ // is `RecommendedPluginsInstructions`, injected by newer codex ahead of
+ // the first prompt once the `recommended_plugins` feature is on.
+ const ENV: &str = "\n /tmp/demo\n";
+ for (label, injected) in [
+ (
+ "agents-with-path",
+ vec![
+ "# AGENTS.md instructions for /tmp/demo\n\n\nhello\n",
+ ENV,
+ ],
+ ),
+ (
+ "agents-pathless",
+ vec![
+ "# AGENTS.md instructions\n\n\nhello\n",
+ ENV,
+ ],
+ ),
+ (
+ "recommended-plugins",
+ vec![
+ "\nHere is a list of plugins that are available but not installed.\n\n- Figma (figma)\n",
+ ],
+ ),
+ ] {
+ let mut content = String::from(
+ "{\"timestamp\":\"2026-03-01T10:00:00Z\",\"type\":\"session_meta\",\"payload\":{\"id\":\"test-1\",\"cwd\":\"/tmp/demo\"}}\n",
+ );
+ content.push_str(&injected_user_record("2026-03-01T10:00:01Z", &injected));
+ content.push_str("{\"timestamp\":\"2026-03-01T10:00:02Z\",\"type\":\"event_msg\",\"payload\":{\"type\":\"user_message\",\"message\":\"真实用户标题\"}}\n");
+ content.push_str("{\"timestamp\":\"2026-03-01T10:00:03Z\",\"type\":\"event_msg\",\"payload\":{\"type\":\"agent_message\",\"message\":\"好的\"}}\n");
- let parser = CodexParser::new();
- let summary = parser
- .parse_jsonl_summary(&path)
- .expect("parse summary ok")
- .expect("summary exists");
- assert_eq!(summary.title.as_deref(), Some("真实用户标题"));
+ let summary = summary_of(&format!("injected-{label}"), &content);
+ assert_eq!(
+ summary.title.as_deref(),
+ Some("真实用户标题"),
+ "{label}: the sidebar titles from the prompt"
+ );
+ assert_eq!(
+ summary.message_count, 2,
+ "{label}: the injection is not a message"
+ );
- let _ = fs::remove_file(path);
+ let detail = parse_rollout(&format!("injected-{label}-detail"), &content, "test-1");
+ assert_eq!(
+ detail.summary.title.as_deref(),
+ Some("真实用户标题"),
+ "{label}: and so does the opened conversation"
+ );
+ assert_eq!(
+ turn_texts(&detail),
+ vec![
+ ("user", Some("真实用户标题".into())),
+ ("assistant", Some("好的".into())),
+ ],
+ "{label}: the injection never renders as a turn"
+ );
+ }
}
#[test]
@@ -13301,6 +13409,7 @@ mod tests {
"\nThe user interrupted the previous turn on purpose.\n",
"agent 3 finished",
"use this",
+ "\nHere is a list of plugins that are available but not installed.\n\n- Figma (figma)\n",
"Warning: apply_patch was requested via exec_command. Use the apply_patch tool instead.",
]
.iter()