SessionStart "compact" hooks are deferred to later turns, causing missing post-compact context and context pollution

Resolved 💬 6 comments Opened Jun 17, 2026 by cowwoc Closed Jul 20, 2026

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

  1. Register a SessionStart hook with matcher compact that:
  • logs full hook payload
  • injects recognizable developer context (for example CAVEMAN=full active every response.)
  1. Use a long-lived Codex session.
  1. 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
  1. Let that turn finish, then send a normal follow-up user turn.
  1. Observe that next turn receives compact hook context multiple times, and hook log records multiple SessionStart invocations with source = "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:

  1. core/src/session/turn.rs
  • when token_limit_reached && needs_follow_up, Codex auto-compacts and continues the same turn
  1. core/src/session/mod.rs
  • each compaction queues SessionStartSource::Compact
  1. 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 compact session-start events
  • those events are deferred instead of firing immediately after compaction
  • next user turn runs SessionStart compact 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:

Black-box integration test:

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

View original on GitHub ↗

6 Comments

cowwoc · 1 month ago

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 compact SessionStart hook is also not delivered immediately at mid-turn auto-compaction time.

From the current control flow:

  • mid-turn auto-compaction queues SessionStartSource::Compact;
  • delivery happens only when run_pending_session_start_hooks() is called at the start of a later run_turn();
  • the same logical turn that actually compacted continues without draining that queued compact source.

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.

Necmttn · 29 days ago

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_id or revision, before/after context hash, and hook_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._

zcb617 · 5 days ago

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:

  1. On UserPromptSubmit, it saves the user's original task instruction, including the goal and hard constraints.
  2. On SessionStart matched to ^compact$, it returns hookSpecificOutput.additionalContext containing that saved task anchor.

Observed behavior

  • After a manual compaction followed by a new user message, SessionStart(source=compact) runs and the anchor is restored as fresh developer context.
  • During an automatic mid-turn compaction, the trace records context_compacted and the agent immediately continues in the same logical turn. The plugin receives no SessionStart(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:

  1. Invoke the recovery hook synchronously after every automatic or manual compaction, before the next model reasoning, output, or tool call.
  2. Allow the hook to return fresh developer context (for example, additionalContext) for that exact task/thread.
  3. If the recovery hook fails, times out, or refuses continuation, pause the task rather than silently continuing without the required anchor.

Fixing immediate SessionStart(source=compact) delivery as proposed here would solve this. An equivalent public API could instead be PostCompact.additionalContext or a synchronous BeforeContinueAfterCompact hook.

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:

  1. the recovery hook ran exactly once;
  2. its returned task anchor is present as fresh developer context; and
  3. a failed recovery hook pauses the task instead of allowing silent continuation.
cowwoc · 3 days ago

It looks like there subagents have a similar problem:

  1. The parent agent spawns the subagent SessionStart
  2. The initial prompt executes
  3. SessionStart only runs after the first turn completes

The expected behavior is for SessionStart to fire before the initial prompt.

cowwoc · 7 hours ago

@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 SubagentStart fires on subagent start and its additionalContext is included in the first request that is sent out. So that seems to be working fine.

cowwoc · 7 hours ago

@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.