SessionStart "compact" hooks are deferred to later turns, causing missing post-compact context and context pollution
Summary
SessionStart hooks matched on compact can fire on a later user turn even when that turn did not compact.
Root cause is that compact session-start sources are queued during a long-running user turn and not delivered immediately at the compaction boundary. If one user request triggers multiple auto-compactions before the turn fully finishes, the next user turn drains all queued compact sources and runs the SessionStart hook once per queued source.
From user perspective, this looks like SessionStart firing "mid-session" without a corresponding compact event at that moment. In the captured JSONL, the injected CAVEMAN=full active every response. text appears shortly after the stray SessionStart fire.
Steps to Reproduce
- Register a
SessionStarthook with matchercompactthat:
- logs full hook payload
- injects recognizable developer context (for example
CAVEMAN=full active every response.)
- Use a long-lived Codex session.
- Send one user request that causes multiple auto-compactions within the same logical user turn. A reliable black-box repro now exists in test:
core/tests/suite/hooks.rs::multiple_auto_compacts_queue_multiple_compact_session_start_hooks_for_next_turn
- Let that turn finish, then send a normal follow-up user turn.
- Observe that next turn receives compact hook context multiple times, and hook log records multiple
SessionStartinvocations withsource = "compact"even though no new compaction happened on that follow-up turn.
Expected Behavior
SessionStart(source=compact) should fire immediately after each actual compaction, including mid-turn auto-compactions, so required post-compaction context is available to the fresh post-compaction agent before work continues.
It should not be queued until the turn completes or replayed on a later user turn.
A later user turn should not see any SessionStart compact hook executions unless that later turn itself follows a new compaction event.
Actual Behavior
Multiple auto-compactions during one long-running user turn queue multiple pending compact session-start sources.
Those queued sources are drained at start of next user turn, causing repeated SessionStart compact hook executions and repeated developer-context injection on a turn that did not itself compact.
This is why a user can see SessionStart fire "mid-conversation" with no nearby compact marker in JSONL.
Environment
- Repo:
openai/codex - Repro base branch:
main - Repro base commit:
0db49a7e6ae8fd459dd8d8d404086c748b9118a1 - Shareable testcase patch on fork:
cowwoc/codex:repro/sessionstart-compact-context-pollution - Shareable testcase commit on fork:
ada5b1b - Compare URL:
https://github.com/cowwoc/codex/compare/0db49a7e6ae8fd459dd8d8d404086c748b9118a1...repro/sessionstart-compact-context-pollution
Analysis
The bug occurs because one logical user turn can queue one or more SessionStartSource::Compact entries, but those entries are not delivered immediately at the compaction boundary. Instead, the next user turn drains whatever compact entries remain queued.
Concrete flow:
core/src/session/turn.rs
- when
token_limit_reached && needs_follow_up, Codex auto-compacts and continues the same turn
core/src/session/mod.rs
- each compaction queues
SessionStartSource::Compact
core/src/hook_runtime.rs
- start of next turn drains all pending session-start sources in a loop
Net effect:
- one logical user turn can queue one or more
compactsession-start events - those events are deferred instead of firing immediately after compaction
- next user turn runs
SessionStartcompact hook once per queued event - user sees delayed hook firing without a contemporaneous compact marker
This also explains why JSONL may show hook fire followed by injected developer context (CAVEMAN=full active every response.) without a nearby compact marker: compact happened earlier, but queued sources were drained later.
Test Coverage Added
White-box deterministic test:
core/src/session/tests.rs::duplicate_pending_compact_sources_fire_multiple_session_start_hooks- https://github.com/cowwoc/codex/blob/repro/sessionstart-compact-context-pollution/codex-rs/core/src/session/tests.rs#L1460
Black-box integration test:
core/tests/suite/hooks.rs::multiple_auto_compacts_queue_multiple_compact_session_start_hooks_for_next_turn- https://github.com/cowwoc/codex/blob/repro/sessionstart-compact-context-pollution/codex-rs/core/tests/suite/hooks.rs#L1435
Black-box test verifies:
- one user request causes 3 real auto-compactions
- next user request receives compact hook injection 3 times
- hook payload log records
["compact", "compact", "compact"]
Example command:
cargo test -p codex-core --test all suite::hooks::multiple_auto_compacts_queue_multiple_compact_session_start_hooks_for_next_turn -- --exact --nocapture
Suggested Fix Direction
Possible fixes:
- change timing so
SessionStart(source=compact)fires immediately after each compaction boundary, including mid-turn auto-compaction - if later-turn draining remains, ensure no stale compact entries survive past the compaction they belong to
- if multiple compact events can occur in one logical turn, preserve one immediate hook delivery per real compaction rather than deferring them all to a later turn
6 Comments
It turns out that this is worse than originally reported.
The current behavior is not just that stale
SessionStart(source=compact)hooks can replay on a later turn. The compactSessionStarthook is also not delivered immediately at mid-turn auto-compaction time.From the current control flow:
SessionStartSource::Compact;run_pending_session_start_hooks()is called at the start of a laterrun_turn();That means any hook that relies on
SessionStart(source=compact)to restore required post-compaction context does not get that context into the fresh post-compaction agent immediately. The agent can therefore continue one or more post-compaction model/tool phases without the rules/context it depends on, and those delayed compact hooks are only injected later on a future turn.So the bug is not only stray later replays / context pollution. It is also missing required immediate post-compaction restoration on the turn where compaction actually happened.
The black-box testcase in this report is consistent with that: one user turn triggers 3 real auto-compactions, and the next user turn receives all 3 compact hook injections. If the compact hook had been delivered immediately after any of those mid-turn compactions, the next turn would not still receive all 3.
Compact SessionStart needs delivery as part of the compaction transaction, not a queued source drained by a later turn.
I would model each compact as a boundary event with
compact_idor revision, before/after context hash, andhook_delivery_status = delivered|deferred|failed. The regression should force two auto-compacts inside one user turn, then assert both hooks fire before the post-compact model resumes and that the next normal turn has no queued compact sources. That catches both current failure modes: missing post-compact context and later context pollution.---
_Generated with ax._
I reproduced the same timing failure in Codex Desktop (
cli_version: 0.144.2) with a small task-anchor plugin.The plugin does two things:
UserPromptSubmit, it saves the user's original task instruction, including the goal and hard constraints.SessionStartmatched to^compact$, it returnshookSpecificOutput.additionalContextcontaining that saved task anchor.Observed behavior
SessionStart(source=compact)runs and the anchor is restored as fresh developer context.context_compactedand the agent immediately continues in the same logical turn. The plugin receives noSessionStart(compact)call and emits no restoration audit event at that boundary. The compact hook is only delivered after a later user turn/re-entry.Therefore,
SessionStart(compact)cannot currently be used as a reliable way to restore task-scoped instructions after every in-progress automatic compaction. This is the same failure mode described in this issue: the resumed agent can continue for one or more reasoning/tool phases without the context that the hook was intended to restore.Why this matters
Long-running complex application-system development often requires multiple compactions. The original task goal and hard constraints need to become fresh context after every compaction; relying only on a progressively compressed summary permits task drift and can lead to violations of those constraints.
This should not require a follow-up user message, project-file changes, or global configuration. The anchor is task/thread scoped state, not project guidance.
Requested behavior
Please make compact restoration part of the compaction transaction:
additionalContext) for that exact task/thread.Fixing immediate
SessionStart(source=compact)delivery as proposed here would solve this. An equivalent public API could instead bePostCompact.additionalContextor a synchronousBeforeContinueAfterCompacthook.Acceptance test
Run a task that needs no additional user messages and force five automatic compactions. For each compaction, verify that, before the first subsequent model reasoning or tool call:
It looks like there subagents have a similar problem:
The expected behavior is for SessionStart to fire before the initial prompt.
@eternal-openai Thank you for fixing this issue! I am looking forward to trying it out.
And to clarify, I think my last comment was incorrect. It looks like
SubagentStartfires on subagent start and itsadditionalContextis included in the first request that is sent out. So that seems to be working fine.@eternal-openai If you're feeling adventurous, please take a look at https://github.com/openai/codex/issues/24849
I would appreciate that finally getting fixed as well.