TUI: /goal 4k-char validation skipped when entire `/goal …` command is pasted as one >1000-char chunk
Summary
When a user pastes a single clipboard string of the form "/goal <large objective>" into an empty composer and the whole paste exceeds LARGE_PASTE_CHAR_THRESHOLD (1000 chars), the TUI never recognizes the input as a /goal slash command. The 4,000-character MAX_THREAD_GOAL_OBJECTIVE_CHARS validation is silently skipped: no error appears, and the literal "/goal …" text is submitted to the model as a regular user message. The model then frequently follows up by calling the create_goal function tool with its own summarized objective, so a goal ends up being created anyway — from the user's perspective the 4k limit looks bypassed.
Observed on codex CLI 0.128.0; reproducible against current main (6a225e400).
Reproduction
- Start a TUI session with the goals feature enabled (empty thread).
- Copy a single string like
/goalfollowed by ~4,001x's to the clipboard (anything that pushes the whole paste over 1000 chars and the objective over 4000 chars). - Paste into an empty composer (the textarea now shows
[Pasted Content N chars]) and press Enter.
Expected: the same Goal objective is too long: 4,001 characters. Limit: 4,000 characters. Put longer instructions in a file … error that fires when the user types /goal first and then pastes the long objective. That path is already covered by goal_slash_command_rejects_large_paste_using_expanded_length at codex-rs/tui/src/chatwidget/tests/goal_validation.rs:133.
Actual:
- No validation error is shown.
- The full
"/goal …"text is submitted as a normaluser_message. - The model often calls the
create_goalfunction tool with a summarized objective, so a goal is created without ever passing through the TUI length guard.
I reproduced this with a one-off test in codex-rs/tui/src/chatwidget/tests/goal_validation.rs that does chat.handle_paste(format!("/goal {}", "x".repeat(MAX_THREAD_GOAL_OBJECTIVE_CHARS + 1))) on an empty composer and submits. SetThreadGoalObjective is never emitted, no Goal objective is too long error is rendered, and a plain Op::UserTurn is sent.
Root cause
In codex-rs/tui/src/bottom_pane/chat_composer.rs:
handle_paste(line 949): any paste withchar_count > LARGE_PASTE_CHAR_THRESHOLD(1000) is replaced by a single[Pasted Content N chars]placeholder element in the textarea, with the full text stashed inpending_pastes.try_dispatch_bare_slash_command(line 3034) andtry_dispatch_slash_command_with_args(line 3070) both inspectself.textarea.text()before expansion and callparse_slash_name, which requires a leading/. When the entire/goal …blob was pasted, the textarea contains only[Pasted Content N chars], so both returnNone.- Execution falls through to
prepare_submission_text_with_options(line 2778), which expands the placeholder. At that point the only length check isMAX_USER_INPUT_TEXT_CHARS(~1 MiB), notMAX_THREAD_GOAL_OBJECTIVE_CHARS. handle_submission_with_timethen emitsInputResult::Submittedwith the expanded"/goal …"text. The slash-dispatch path that callsgoal_objective_with_pending_pastes_is_allowed(codex-rs/tui/src/chatwidget/goal_validation.rs:17) is never entered.
Existing paste tests miss this case because they pre-fill "/goal " into the composer before pasting, so the textarea already starts with /:
goal_slash_command_rejects_large_paste_using_expanded_length(line 133)goal_slash_command_giant_paste_uses_goal_specific_error(line 166)
Note: the protocol-side validate_thread_goal_objective (codex-rs/protocol/src/protocol.rs:3551) does enforce 4k on create_goal arguments, but the model may summarize the objective before calling the tool, so the protocol guard often does not trip for this path. It is also not user-visible feedback at the moment of submission.
Why it matters
- Silent failure mode: the user pastes a long
/goal, sees no error, and may not realize the goal that gets set (if any) was paraphrased by the model rather than created from their literal text. - Token waste: the full pasted text is round-tripped through the model even though the TUI guard was meant to short-circuit it.
- Inconsistent with the existing pasted-after-
/goalbehavior, which was the whole point of #20746.
Proposed fix direction
Make the slash-command detector consider the expanded text when the composer leads with a paste placeholder. Two reasonable shapes:
- In
try_dispatch_bare_slash_command/try_dispatch_slash_command_with_args, when the raw textarea starts with a[Pasted Content …]placeholder, parseparse_slash_nameagainst the expansion (or against the first pending-paste content) and, if it resolves to a known builtin, route through the normal slash-dispatch path sogoal_objective_with_pending_pastes_is_allowedruns. - Alternatively, in
prepare_submission_text_with_options, after expansion, if the resulting text starts with a known slash command, redirect into the slash-dispatch path instead of returningInputResult::Submitted.
Either path should also pick up a regression test mirroring goal_slash_command_rejects_large_paste_using_expanded_length but with the entire "/goal " + objective pasted into an empty composer.
Happy to send a PR if a maintainer agrees with one of the directions.
3 Comments
@etraut-openai tagging you here because of this comment in another thread https://github.com/openai/codex/issues/20598#issuecomment-4367177939
It seems that pasting the /goal command bypasses the limit
Yes, this is expected behavior. When you paste text into the composer, it will not be treated as a slash command or go through normal slash command processing. You are instead sending the full pasted text to the model. In this case, the model is interpreting this as a request to create a new goal, and it does to using the
create_goaltool. It knows about the 4K limit so it will limit the objective size (typically through summarization) to avoid going over.Thanks for explaining.
I noticed this issue because I asked codex to write the goal prompt for before pasting it in.
What is better, pasting the goal in triggering summarization or putting it in an md file?
If it's the latter, it would be nice if codex indicated to put the goal in an md file like it does when the user doesn't paste anything.