Skip to content

[hooks] use a user message > developer message for prompt continuation#14867

Merged
eternal-openai merged 14 commits intomainfrom
eternal/hooks_user_message_xml
Mar 19, 2026
Merged

[hooks] use a user message > developer message for prompt continuation#14867
eternal-openai merged 14 commits intomainfrom
eternal/hooks_user_message_xml

Conversation

@eternal-openai
Copy link
Contributor

@eternal-openai eternal-openai commented Mar 17, 2026

Summary

Persist Stop-hook continuation prompts as user messages instead of hidden developer messages + some requested integration tests

This is a followup to @pakrym 's comment in #14532 to make sure stop-block continuation prompts match training for turn loops

  • Stop continuation now writes <hook_prompt hook_run_id="...">stop hook's user prompt<hook_prompt>
  • Introduces quick-xml dependency, though we already indirectly depended on it anyway via syntect
  • This PR only has about 500 lines of actual logic changes, the rest is tests/schema

Testing

Example run (with a sessionstart hook and 3 stop hooks) - this shows context added by session start, then two stop hooks sending their own additional prompts in a new turn. The model responds with a single message addressing both. Then when that turn ends, the hooks detect that they just ran using stop_hook_active and decide not to infinite loop

test files for this (unzip, move codex -> .codex): codex.zip

› cats


• Running SessionStart hook: lighting the observatory

SessionStart hook (completed)
  warning: Hi, I'm a session start hook for wizard-tower (startup).
  hook context: A wimboltine stonpet is an exotic cuisine from hyperspace

• Cats are tiny zen wizards, my friend: equal parts nap, mystery, and chaos. If you want, we can talk
  cat facts, cat breeds, cat names, or build something cat-themed in this repo.

• Running Stop hook: checking the tower wards

• Running Stop hook: sacking the guards

• Running Stop hook: hiring the guards

Stop hook (completed)
  warning: Wizard Tower Stop hook reviewed the completed reply (177 chars).

Stop hook (blocked)
  warning: Wizard Tower Stop hook continuing conversation
  feedback: cook the stonpet

Stop hook (blocked)
  warning: Wizard Tower Stop hook continuing conversation
  feedback: eat the cooked stonpet

• Stonpet’s cooked, aloha style: flash-seared over a blue quasiflame, glazed with nebula salt, and
  rested until the hyperspace juices settle.

  Now we eat with gratitude, my friend. One mindful bite in, and the flavor is pure cosmic surf:
  smoky, bright, and totally out of this dimension.

• Running Stop hook: checking the tower wards

• Running Stop hook: sacking the guards

• Running Stop hook: hiring the guards

Stop hook (completed)
  warning: Wizard Tower Stop hook reviewed the completed reply (285 chars).

Stop hook (completed)
  warning: Wizard Tower Stop hook saw a second pass and stayed calm to avoid a loop.

Stop hook (completed)
  warning: Wizard Tower Stop hook saw a second pass and stayed calm to avoid a loop.

# Conflicts:
#	codex-rs/app-server/src/bespoke_event_handling.rs
#	codex-rs/core/src/contextual_user_message.rs
#	codex-rs/core/src/contextual_user_message_tests.rs
@eternal-openai eternal-openai force-pushed the eternal/hooks_user_message_xml branch from af14778 to 7b8c000 Compare March 17, 2026 19:08
@eternal-openai eternal-openai marked this pull request as ready for review March 18, 2026 04:33
matches!(
crate::event_mapping::parse_turn_item(item),
Some(TurnItem::UserMessage(_))
Some(TurnItem::UserMessage(_) | TurnItem::HookPrompt(_))
Copy link
Collaborator

@sayan-oai sayan-oai Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also persist HookPrompt turn items for the local compaction used by non OpenAI providers here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they should be persisted in both types of compaction. For remote compaction we filter out "special" user messages like content information but hook prompts might need to be persisted because they start the turn.

@sayan-oai
Copy link
Collaborator

now stop hooks are user messages that are persisted across compaction. IIRC, sessionstart + userpromptsubmit hooks are still dev messages? should those be converted as well?

@eternal-openai
Copy link
Contributor Author

now stop hooks are user messages that are persisted across compaction. IIRC, sessionstart + userpromptsubmit hooks are still dev messages? should those be converted as well?

@sayan-oai not quite, I only user-message-ified the stop-block continuation prompts, not the hook runs themselves. So this is when a Stop hook returns decision: block, reason: new prompt here that triggers a new turn, and that new message is the only thing that's a user message

That said, I am generally in favor of persisting hook runs as a whole, but Pavel has some larger plans for persisting metadata, so currently they are fully ephemeral

@sayan-oai
Copy link
Collaborator

now stop hooks are user messages that are persisted across compaction. IIRC, sessionstart + userpromptsubmit hooks are still dev messages? should those be converted as well?

@sayan-oai not quite, I only user-message-ified the stop-block continuation prompts, not the hook runs themselves. So this is when a Stop hook returns decision: block, reason: new prompt here that triggers a new turn, and that new message is the only thing that's a user message

That said, I am generally in favor of persisting hook runs as a whole, but Pavel has some larger plans for persisting metadata, so currently they are fully ephemeral

ah this makes sense. the current distinction seems reasonable then.

notification.item,
ThreadItem::HookPrompt {
id: notification.item.id().to_string(),
fragments: vec![
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider content to align with user message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do in a followup

assert_eq!(notification.turn_id, "turn-1");
assert_eq!(
notification.item,
ThreadItem::HookPrompt {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider HookMessage

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do in a followup

#[ts(rename_all = "camelCase", export_to = "v2/")]
pub struct HookPromptFragment {
pub text: String,
pub hook_run_id: String,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we expose this ID anywhere else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in the UIs, this is just to make the data model clean

if hook_run_id.trim().is_empty() {
return None;
}
to_xml_string(&HookPromptXml {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do not typically use real XML serialization for context injection, we don't care about value escaping, just rough boundaries.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see. Probably sounds like it's worth doing in case there are xml tags in the new user prompt. I imagine that's a bit paranoid, but probably safe?

}
to_xml_string(&HookPromptXml {
text: text.to_string(),
hook_run_id: hook_run_id.to_string(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this end up in model context? Is it useful there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is just to be able to link the hook run to the prompt, but if you don't think that's important, I can remove it

@eternal-openai eternal-openai merged commit 267499b into main Mar 19, 2026
33 checks passed
@eternal-openai eternal-openai deleted the eternal/hooks_user_message_xml branch March 19, 2026 17:53
@github-actions github-actions bot locked and limited conversation to collaborators Mar 19, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants