mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
Add compact lifecycle hooks (started by vincentkoc - external contrib) (#19905)
Based on work from Vincent K - https://github.com/openai/codex/pull/19060 <img width="1836" height="642" alt="CleanShot 2026-04-29 at 20 47 40@2x" src="https://github.com/user-attachments/assets/b647bb89-65fe-40c8-80b0-7a6b7c984634" /> ## Why Compaction rewrites the conversation context that future model turns receive, but hooks currently have no deterministic lifecycle point around that rewrite. This adds compact lifecycle hooks so users can audit manual and automatic compaction, surface hook messages in the UI, and run post-compaction follow-up without overloading tool or prompt hooks. ## What Changed - Added `PreCompact` and `PostCompact` hook events across hook config, discovery, dispatch, generated schemas, app-server notifications, analytics, and TUI hook rendering. - Added trigger matching for compact hooks with the documented `manual` and `auto` matcher values. - Wired `PreCompact` before both local and remote compaction, and `PostCompact` after successful local or remote compaction. - Kept compact hook command input to lifecycle metadata: session id, Codex turn id, transcript path, cwd, hook event name, model, and trigger. - Made compact stdout handling consistent with other hooks: plain stdout is ignored as debug output, while malformed JSON-looking stdout is reported as failed hook output. - Added integration coverage for compact hook dispatch, trigger matching, post-compact execution, and the audited behavior that `decision:"block"` does not block compaction. ## Out of Scope - Hook-specific compaction blocking is not implemented; `decision:"block"` and exit-code-2 blocking semantics are intentionally unsupported for `PreCompact`. - Custom compaction instructions are not exposed to compact hooks in this PR. - Compact summaries, summary character counts, and summary previews are not exposed to compact hooks in this PR. ## Verification - `cargo test -p codex-hooks` - `cargo test -p codex-core manual_pre_compact_block_decision_does_not_block_compaction` - `cargo test -p codex-app-server hooks_list` - `cargo test -p codex-core config_schema_matches_fixture` - `cargo test -p codex-tui hooks_browser` ## Docs The developer documentation for Codex hooks should be updated alongside this feature to document `PreCompact` and `PostCompact`, the `manual`/`auto` matcher values, and the compact hook payload fields. --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -17,8 +17,12 @@ const POST_TOOL_USE_INPUT_FIXTURE: &str = "post-tool-use.command.input.schema.js
|
||||
const POST_TOOL_USE_OUTPUT_FIXTURE: &str = "post-tool-use.command.output.schema.json";
|
||||
const PERMISSION_REQUEST_INPUT_FIXTURE: &str = "permission-request.command.input.schema.json";
|
||||
const PERMISSION_REQUEST_OUTPUT_FIXTURE: &str = "permission-request.command.output.schema.json";
|
||||
const POST_COMPACT_INPUT_FIXTURE: &str = "post-compact.command.input.schema.json";
|
||||
const POST_COMPACT_OUTPUT_FIXTURE: &str = "post-compact.command.output.schema.json";
|
||||
const PRE_TOOL_USE_INPUT_FIXTURE: &str = "pre-tool-use.command.input.schema.json";
|
||||
const PRE_TOOL_USE_OUTPUT_FIXTURE: &str = "pre-tool-use.command.output.schema.json";
|
||||
const PRE_COMPACT_INPUT_FIXTURE: &str = "pre-compact.command.input.schema.json";
|
||||
const PRE_COMPACT_OUTPUT_FIXTURE: &str = "pre-compact.command.output.schema.json";
|
||||
const SESSION_START_INPUT_FIXTURE: &str = "session-start.command.input.schema.json";
|
||||
const SESSION_START_OUTPUT_FIXTURE: &str = "session-start.command.output.schema.json";
|
||||
const USER_PROMPT_SUBMIT_INPUT_FIXTURE: &str = "user-prompt-submit.command.input.schema.json";
|
||||
@@ -75,6 +79,10 @@ pub(crate) enum HookEventNameWire {
|
||||
PermissionRequest,
|
||||
#[serde(rename = "PostToolUse")]
|
||||
PostToolUse,
|
||||
#[serde(rename = "PreCompact")]
|
||||
PreCompact,
|
||||
#[serde(rename = "PostCompact")]
|
||||
PostCompact,
|
||||
#[serde(rename = "SessionStart")]
|
||||
SessionStart,
|
||||
#[serde(rename = "UserPromptSubmit")]
|
||||
@@ -124,6 +132,24 @@ pub(crate) struct PermissionRequestCommandOutputWire {
|
||||
pub hook_specific_output: Option<PermissionRequestHookSpecificOutputWire>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[schemars(rename = "pre-compact.command.output")]
|
||||
pub(crate) struct PreCompactCommandOutputWire {
|
||||
#[serde(flatten)]
|
||||
pub universal: HookUniversalOutputWire,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[schemars(rename = "post-compact.command.output")]
|
||||
pub(crate) struct PostCompactCommandOutputWire {
|
||||
#[serde(flatten)]
|
||||
pub universal: HookUniversalOutputWire,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -267,6 +293,38 @@ pub(crate) struct PostToolUseCommandInput {
|
||||
pub tool_use_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[schemars(rename = "pre-compact.command.input")]
|
||||
pub(crate) struct PreCompactCommandInput {
|
||||
pub session_id: String,
|
||||
/// Codex extension: expose the active turn id to internal turn-scoped hooks.
|
||||
pub turn_id: String,
|
||||
pub transcript_path: NullableString,
|
||||
pub cwd: String,
|
||||
#[schemars(schema_with = "pre_compact_hook_event_name_schema")]
|
||||
pub hook_event_name: String,
|
||||
pub model: String,
|
||||
#[schemars(schema_with = "compaction_trigger_schema")]
|
||||
pub trigger: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[schemars(rename = "post-compact.command.input")]
|
||||
pub(crate) struct PostCompactCommandInput {
|
||||
pub session_id: String,
|
||||
/// Codex extension: expose the active turn id to internal turn-scoped hooks.
|
||||
pub turn_id: String,
|
||||
pub transcript_path: NullableString,
|
||||
pub cwd: String,
|
||||
#[schemars(schema_with = "post_compact_hook_event_name_schema")]
|
||||
pub hook_event_name: String,
|
||||
pub model: String,
|
||||
#[schemars(schema_with = "compaction_trigger_schema")]
|
||||
pub trigger: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -424,6 +482,22 @@ pub fn write_schema_fixtures(schema_root: &Path) -> anyhow::Result<()> {
|
||||
&generated_dir.join(PERMISSION_REQUEST_OUTPUT_FIXTURE),
|
||||
schema_json::<PermissionRequestCommandOutputWire>()?,
|
||||
)?;
|
||||
write_schema(
|
||||
&generated_dir.join(POST_COMPACT_INPUT_FIXTURE),
|
||||
schema_json::<PostCompactCommandInput>()?,
|
||||
)?;
|
||||
write_schema(
|
||||
&generated_dir.join(POST_COMPACT_OUTPUT_FIXTURE),
|
||||
schema_json::<PostCompactCommandOutputWire>()?,
|
||||
)?;
|
||||
write_schema(
|
||||
&generated_dir.join(PRE_COMPACT_INPUT_FIXTURE),
|
||||
schema_json::<PreCompactCommandInput>()?,
|
||||
)?;
|
||||
write_schema(
|
||||
&generated_dir.join(PRE_COMPACT_OUTPUT_FIXTURE),
|
||||
schema_json::<PreCompactCommandOutputWire>()?,
|
||||
)?;
|
||||
write_schema(
|
||||
&generated_dir.join(PRE_TOOL_USE_INPUT_FIXTURE),
|
||||
schema_json::<PreToolUseCommandInput>()?,
|
||||
@@ -519,6 +593,14 @@ fn post_tool_use_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("PostToolUse")
|
||||
}
|
||||
|
||||
fn pre_compact_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("PreCompact")
|
||||
}
|
||||
|
||||
fn post_compact_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("PostCompact")
|
||||
}
|
||||
|
||||
fn pre_tool_use_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("PreToolUse")
|
||||
}
|
||||
@@ -549,6 +631,10 @@ fn session_start_source_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_enum_schema(&["startup", "resume", "clear"])
|
||||
}
|
||||
|
||||
fn compaction_trigger_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_enum_schema(&["manual", "auto"])
|
||||
}
|
||||
|
||||
fn string_const_schema(value: &str) -> Schema {
|
||||
let mut schema = SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
@@ -580,12 +666,18 @@ fn default_continue() -> bool {
|
||||
mod tests {
|
||||
use super::PERMISSION_REQUEST_INPUT_FIXTURE;
|
||||
use super::PERMISSION_REQUEST_OUTPUT_FIXTURE;
|
||||
use super::POST_COMPACT_INPUT_FIXTURE;
|
||||
use super::POST_COMPACT_OUTPUT_FIXTURE;
|
||||
use super::POST_TOOL_USE_INPUT_FIXTURE;
|
||||
use super::POST_TOOL_USE_OUTPUT_FIXTURE;
|
||||
use super::PRE_COMPACT_INPUT_FIXTURE;
|
||||
use super::PRE_COMPACT_OUTPUT_FIXTURE;
|
||||
use super::PRE_TOOL_USE_INPUT_FIXTURE;
|
||||
use super::PRE_TOOL_USE_OUTPUT_FIXTURE;
|
||||
use super::PermissionRequestCommandInput;
|
||||
use super::PostCompactCommandInput;
|
||||
use super::PostToolUseCommandInput;
|
||||
use super::PreCompactCommandInput;
|
||||
use super::PreToolUseCommandInput;
|
||||
use super::SESSION_START_INPUT_FIXTURE;
|
||||
use super::SESSION_START_OUTPUT_FIXTURE;
|
||||
@@ -615,6 +707,18 @@ mod tests {
|
||||
PERMISSION_REQUEST_OUTPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/permission-request.command.output.schema.json")
|
||||
}
|
||||
POST_COMPACT_INPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/post-compact.command.input.schema.json")
|
||||
}
|
||||
POST_COMPACT_OUTPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/post-compact.command.output.schema.json")
|
||||
}
|
||||
PRE_COMPACT_INPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/pre-compact.command.input.schema.json")
|
||||
}
|
||||
PRE_COMPACT_OUTPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/pre-compact.command.output.schema.json")
|
||||
}
|
||||
PRE_TOOL_USE_INPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/pre-tool-use.command.input.schema.json")
|
||||
}
|
||||
@@ -658,6 +762,10 @@ mod tests {
|
||||
POST_TOOL_USE_OUTPUT_FIXTURE,
|
||||
PERMISSION_REQUEST_INPUT_FIXTURE,
|
||||
PERMISSION_REQUEST_OUTPUT_FIXTURE,
|
||||
POST_COMPACT_INPUT_FIXTURE,
|
||||
POST_COMPACT_OUTPUT_FIXTURE,
|
||||
PRE_COMPACT_INPUT_FIXTURE,
|
||||
PRE_COMPACT_OUTPUT_FIXTURE,
|
||||
PRE_TOOL_USE_INPUT_FIXTURE,
|
||||
PRE_TOOL_USE_OUTPUT_FIXTURE,
|
||||
SESSION_START_INPUT_FIXTURE,
|
||||
@@ -688,6 +796,14 @@ mod tests {
|
||||
.expect("serialize post tool use input schema"),
|
||||
)
|
||||
.expect("parse post tool use input schema");
|
||||
let pre_compact: Value = serde_json::from_slice(
|
||||
&schema_json::<PreCompactCommandInput>().expect("serialize pre compact input schema"),
|
||||
)
|
||||
.expect("parse pre compact input schema");
|
||||
let post_compact: Value = serde_json::from_slice(
|
||||
&schema_json::<PostCompactCommandInput>().expect("serialize post compact input schema"),
|
||||
)
|
||||
.expect("parse post compact input schema");
|
||||
let permission_request: Value = serde_json::from_slice(
|
||||
&schema_json::<PermissionRequestCommandInput>()
|
||||
.expect("serialize permission request input schema"),
|
||||
@@ -707,6 +823,8 @@ mod tests {
|
||||
&pre_tool_use,
|
||||
&permission_request,
|
||||
&post_tool_use,
|
||||
&pre_compact,
|
||||
&post_compact,
|
||||
&user_prompt_submit,
|
||||
&stop,
|
||||
] {
|
||||
|
||||
Reference in New Issue
Block a user