Context-fragment tag literals are duplicated between injection and recognition sites, allowing silent drift
Summary
Several contextual-fragment tags are hardcoded in two places each: once in the fragment's type_markers() (used when injecting the fragment into the conversation) and again in CONTEXTUAL_DEVELOPER_PREFIXES in core/src/event_mapping.rs (used when recognizing previously injected fragments). There is no shared constant tying the two sites together, so editing one without the other compiles cleanly but silently breaks fragment recognition.
The repo already has a designated home for these strings — the tag-constant block in protocol/src/protocol.rs, whose comment says "Used across crates to avoid duplicated hardcoded strings" — but these tags never got moved there:
Duplicated tags
CONTEXTUAL_DEVELOPER_PREFIXES mixes constant references with re-hardcoded literals:
The re-hardcoded ones and their type_markers() counterparts:
| Tag | Injection site (type_markers()) | Recognition site |
|---|---|---|
| <permissions instructions> | prompts/src/permissions_instructions.rs (L152-L154) | core/src/event_mapping.rs L32 |
| <model_switch> | core/src/context/model_switch_instructions.rs (L25-L27) | core/src/event_mapping.rs L33 |
| <personality_spec> | core/src/context/personality_spec_instructions.rs (L23-L25) | core/src/event_mapping.rs L38 |
| <rollout_budget> | core/src/context/rollout_budget.rs (L17-L19) | core/src/event_mapping.rs L43 |
<permissions instructions> is the most notable case because the duplication crosses crate boundaries (codex-prompts ↔ codex-core), so the two sites don't even live near each other. (It is also the only tag whose name contains a space rather than underscores, which is presumably frozen for compatibility — this issue does not propose changing any tag strings.)
"<token_budget>" in the same list appears intentional (comment: legacy recognition for older persisted rollouts) and is excluded.
Failure mode
If a future change renames or tweaks a tag at one site only, nothing fails at compile time or in most tests. Fragments injected with the new tag are no longer matched by is_contextual_dev_fragment / CONTEXTUAL_DEVELOPER_PREFIXES, so those developer messages stop being treated as contextual fragments (affecting rollback trimming and message filtering) — a silent behavioral regression.
Proposed fix (high-level outline)
Purely mechanical, no behavior change, all tag strings stay byte-identical:
- Add the missing constants to the existing tag block in
protocol/src/protocol.rs, e.g.PERMISSIONS_INSTRUCTIONS_OPEN_TAG/_CLOSE_TAG,MODEL_SWITCH_OPEN_TAG/_CLOSE_TAG,PERSONALITY_SPEC_OPEN_TAG/_CLOSE_TAG,ROLLOUT_BUDGET_OPEN_TAG/_CLOSE_TAG. - Reference the constants from each
type_markers()implementation and fromCONTEXTUAL_DEVELOPER_PREFIXES, matching howCOLLABORATION_MODE_OPEN_TAGetc. are already used there.
One nuance: RolloutBudgetContext::type_markers() currently embeds newlines in the markers ("<rollout_budget>\n" / "\n</rollout_budget>"). Since &'static str constants can't be concatenated with \n in a const context, the newlines move into body(), keeping the rendered output byte-identical. Recognition via matches_marked_text (prefix/suffix match on trimmed text) is unaffected.
I have this change working locally (6 files, ~+35/−9; existing tests that assert the literal tag strings are intentionally left untouched so they still pin the wire format; codex-protocol, codex-prompts, and the targeted codex-core test suites pass) and would be happy to submit it as a PR if invited.