Add a synchronous hook between compaction decision and context replacement
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
What variant of Codex are you using?
CLI
What feature would you like to see?
I’d like Codex to expose a synchronous extension point between the decision to perform context compaction and the replacement of the current context.
The key requirement is ordering: once Codex has selected a context transition, it should be possible to run an optional extension and wait for it to complete before the old context is replaced.
Conceptually:
compaction decision
↓
PreCompact
↓
[ NEW: synchronous pre-replacement extension point ]
↑
requested boundary
↓
extension completes / returns result
↓
native context replacement
↓
PostCompact / SessionStart
The important invariant is:
extension completed
before
context replacement started
Why this lifecycle point is useful
Some pre-transition state can only be captured reliably before the current context is summarized or replaced.
After compaction, extensions can work with the compacted state, but that may no longer represent the exact state that existed immediately before the transition.
A supported synchronous boundary here would make it possible to safely implement workflows such as:
- durable capture of current task or execution state;
- thread/session handoff or migration to a fresh context;
- external memory synchronization;
- audit or evidence capture;
- transition validation;
- policy or persistence checks that must complete before replacement.
The extension point itself would not need to define a checkpoint format, memory system, or handoff mechanism. Those should remain application-specific.
What is missing today
The existing lifecycle points are close, but do not provide this specific guarantee.
PreCompact is useful as a pre-compaction lifecycle/policy hook, while PostCompact and SessionStart(source=compact) operate after the transition.
An external process can also watch token usage and try to prepare state before automatic compaction, but that creates a race: native compaction can begin while the external work is still running. Triggering earlier gives more headroom, but does not guarantee ordering.
The requested capability is therefore narrower than a new compaction strategy:
Codex selects a context transition
↓
optional synchronous pre-transition work
↓
Codex waits for completion
↓
only then may context replacement begin
This would add a missing lifecycle boundary without requiring changes to the native compaction algorithm itself.
Additional information
I reached this request after looking at a long-running compaction workflow where an external process tried to capture current task state before automatic compaction.
The practical problem was a race:
external process detects high context usage
↓
pre-transition state capture starts
↓
native automatic compaction becomes eligible
↓
native context replacement may begin
↓
external capture is still running
Moving the external trigger earlier gives more headroom, but it cannot guarantee ordering. That led me to look for a lifecycle boundary inside Codex itself.
Existing lifecycle points
The existing hooks are related, but have different semantics.
PreCompact
PreCompact already runs before compaction and can wait for synchronous handlers.
Its current contract, however, is primarily continue/stop lifecycle behavior. Stop aborts the current turn.
The missing capability I was looking for is slightly different:
a context transition has been selected
↓
complete required pre-replacement work
↓
return success/failure
↓
continue that same transition
In other words, I am not trying to replace PreCompact; I am trying to identify a distinct transactional boundary between policy and replacement.
PostCompact / SessionStart(source=compact)
These are useful for restoration, reinjection, and post-transition processing, but they run after the old context has already been replaced.
For workflows that need the exact pre-transition state, that is necessarily too late.
Local proof of concept
To check whether such a lifecycle boundary is technically practical, I built a small local prototype against:
Codex rust-v0.153.2
commit 657a993cbee87acf52d14b758ce49dbd46d1b8eb
I used the local name BeforeContextTransition for the experimental event, but I do not consider that name or API shape important to the request.
The prototype inserts a synchronous hook after PreCompact and before the native compaction implementation proceeds toward context replacement:
PreCompact
↓
await BeforeContextTransition
↓
native compaction implementation / replacement
It reuses the existing Codex hook infrastructure:
- hook dispatcher;
- command runner;
- synchronous execution;
- timeout handling;
HookStarted/HookCompletedevents.
The external hook receives transition metadata only. The full conversation history is not passed to the hook.
The prototype wires this gate into source for:
- local Responses compaction;
- legacy remote compaction;
- Remote Compaction V2;
- TokenBudget compaction.
Only Remote Compaction V2 has been exercised with a real runtime so far.
Current prototype behavior
The experimental hook receives metadata roughly equivalent to:
transition_id
thread_id
source_turn_id
trigger
reason
phase
compaction_path
and can return roughly:
continue + optional opaque receipt
or
abort + reason
The current prototype uses strict failure behavior:
hook success
→ continue toward replacement
hook failure / invalid output / timeout
→ do not continue into replacement
If PreCompact stops the transition, the experimental hook is not invoked.
The prototype also keeps the pre-transition conversation owned by Codex rather than exposing the full conversation history to the external command.
Unit-level evidence
The current generic-hook prototype tests cover:
- transition metadata serialization;
- excluding conversation history from external hook input;
- opaque receipt parsing;
- failure handling;
- invalid output handling;
- timeout behavior;
- hook matcher behavior.
An earlier marker-only version of the prototype also used an internal ordered event test that verified:
callback_completed < replacement_started
That direct replacement-sequence assertion is not retained in the current generic-hook refactor, so I do not want to overstate the current unit-test coverage.
The source call sites currently enforce the intended ordering by awaiting the hook before proceeding into the compaction implementation.
Real-runtime validation
I also ran the configured hook against a real Codex runtime in an isolated temporary environment.
The runtime used Remote Compaction V2.
Manual compaction
Observed sequence:
configured hook started
→ configured hook completed
→ ContextCompaction started
→ ContextCompaction completed
The compaction turn completed successfully, and the thread remained usable afterward.
Automatic context-limit compaction
Observed sequence:
configured hook started
→ configured hook completed
→ ContextCompaction
→ same-turn continuation
→ agent response
→ turn completed
So the automatic case confirmed that the configured synchronous hook can run before native compaction and that normal work can continue afterward in the same active turn.
These tests were only intended to check whether the lifecycle boundary is practical. They are not a complete checkpointing or continuity implementation.
Current prototype limitations
The prototype intentionally does not claim production semantics for several things.
It does not currently implement:
- durable transition identity across retry or restart;
- restart-safe exactly-once producer execution;
- propagation of transition identity to
PostCompactorSessionStart; - app-server exposure of transition identity;
- durable or externally readable receipts;
- real-runtime E2E for local, legacy remote, or TokenBudget paths;
- a standard checkpoint or handoff format;
- a model-backed pre-transition state producer.
For example, the current transition_id is only a core-generated UUID for one in-process transition invocation.
Similarly, the experimental hook can return an opaque receipt, but the prototype currently parses it for gate success and then discards it. Receipt persistence and correlation with compaction events are separate design questions.
Related issues
I found several closely related discussions:
-
#17148 — Pre and PostCompact hooks
Covers lifecycle hooks around compaction, including state-capture and reinjection use cases. The narrower gap here is the explicit guarantee that Codex waits for synchronous pre-replacement work before replacement begins. -
#19061 — Add a post-compaction hook for deterministic memory reinjection
Covers the post-replacement side of the lifecycle and was completed with a post-compaction hook. This request concerns the opposite boundary: synchronous work that must finish before context replacement begins. -
#28633 — PreCompact/PostCompact hook receipts are not observable around compaction events
Focuses on observability and durable evidence that hooks ran, failed, timed out, or were skipped. This is closely related to receipt design, but not to using hook completion itself as a replacement gate. -
#25074 — Auto-run a skill when context is near exhaustion before compaction
Has a very similar handoff motivation, but is based on threshold-triggered preparation. The request here is for the lower-level native lifecycle boundary after a context transition has actually been selected. -
#37699 — Treat context compaction as a trust-boundary transition
Focuses on mutation safety and state verification across compaction. This request concerns synchronous work before the old context is replaced. -
#36669 — Model-callable context management tools for active, selective compaction
Concerns model-driven context management rather than this lifecycle boundary. -
#22036 — BeforeModelRequest context-transform hook
Proposes a synchronous extension point at another lifecycle boundary. The pattern is similar, but the requested boundary here is specifically context replacement during compaction.
I could not find an existing request that defines the exact ordering guarantee described here:
selected context transition
↓
synchronous extension completes
↓
only then may context replacement begin
Scope
I am intentionally trying to keep this request limited to the lifecycle boundary itself.
I am not asking Codex to:
- change the native compaction or summarization algorithm;
- define a checkpoint or handoff schema;
- own a particular external-memory implementation;
- automatically migrate work to another thread or session;
- expose the full pre-compaction conversation history to an external hook;
- guarantee that one specific continuity strategy preserves all task state.
Those are possible consumers of the boundary, not requirements of the boundary itself.
Questions
I would mainly like maintainer guidance on the intended extension model here:
- Is a synchronous lifecycle point between compaction selection and context replacement a reasonable supported boundary?
- Would this be better represented as a separate semantic event, or should
PreCompacteventually cover this behavior? - Would a per-transition identity be appropriate for correlating pre-transition work with the later compaction lifecycle?
- Should failure at this boundary be strictly aborting, or should the contract support both strict and best-effort behavior?
- Would reusing the existing hook dispatcher and synchronous command execution be the right direction, or is there another extension mechanism that would fit better?
I am not proposing that the local prototype API be adopted as-is. The prototype was mainly used to verify that the lifecycle boundary is implementable and to make the missing ordering guarantee concrete.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing the existing PreCompact, PostCompact, and SessionStart lifecycle points, then trace the compaction paths and shared hook dispatcher described in the issue. Compare the prototype’s ordering and unit-level evidence with current behavior. Done would require an agreed synchronous boundary, failure contract, and tests proving completion precedes context replacement.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100