Skip to content

fix(compression): anchor compaction on the latest actionable user turn - #69291

Merged
teknium1 merged 3 commits into
mainfrom
salvage/62747-actionable-user-turn
Jul 22, 2026
Merged

fix(compression): anchor compaction on the latest actionable user turn#69291
teknium1 merged 3 commits into
mainfrom
salvage/62747-actionable-user-turn

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Context compaction now anchors on the latest actionable user turn, so a blank platform-echo user row can no longer displace the user's real request (e.g. an async-completion event) into the summarized middle. Root cause: _find_last_user_message_idx anchored on ANY non-summary user row, including genuinely empty platform echoes.

Changes

  • agent/context_compressor.py: new _is_actionable_user_turn classifier (conservative: image/audio/unknown structured blocks count as actionable; only genuinely empty text is blank) and _blank_echo_indices_after (removes blank echoes only when contiguous after the actionable event and assistant-followed); tail anchor now uses actionability; bridge retention guarded by bridge_idx > compress_start; docstring note that blank-echo removal runs in the cheap pre-pass and therefore survives a summary abort (precedented by Phase-1 pruning).
  • tests/agent/test_compressor_actionable_tail_anchor.py: new suite covering blank-echo displacement, leading blanks, input_text blanks, multimodal actionability, bridge retention.
  • tests/agent/test_context_compressor.py: one expectation updated (dropped_count 3→2) — intentional behavior change: the blank echo is now stripped pre-summary instead of counted as dropped.

Validation

Before After
Blank platform echo after real user event Echo anchors the tail; real request summarized away Echo removed; real actionable turn anchored in tail
Aborted compression Still strips blank echoes (documented, matches Phase-1 pruning precedent)

Targeted tests: tests/agent/ -k 'actionable or anchor or last_user' → 56 passed; actionable-anchor + assistant-anchor + context-compressor suites → 222 passed, 0 failed.

Credit

Salvaged from #62747 by @John-Lussier.

Infographic

actionable-user-turn-anchor

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

running on ea2e9e5

looks good to me!

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 22, 2026
@teknium1
teknium1 force-pushed the salvage/62747-actionable-user-turn branch from 995bf9a to ea2e9e5 Compare July 22, 2026 14:08
@teknium1
teknium1 merged commit 2ee50c6 into main Jul 22, 2026
36 checks passed
@teknium1
teknium1 deleted the salvage/62747-actionable-user-turn branch July 22, 2026 15:10
teknium1 added a commit that referenced this pull request Jul 23, 2026
…or-preserving default

Follow-up fixes on top of the salvaged #22566 mechanism:

- N-collector now counts only REAL actionable user turns via
  _is_actionable_user_turn + _is_synthetic_compression_user_turn —
  the same filter pair _find_last_user_message_idx uses post-#69291.
  The contributor's bare role=='user' + _is_context_summary_content
  check let blank platform echoes and continuation/todo rows consume
  N slots, silently degrading the guarantee.
- Default flipped 3 -> 1 (behavior-preserving): a default of 3 was
  measured to change the tail cut on transcripts whose budget covers
  only the last turn. min_tail_user_messages=1 delegates to the
  existing single-user anchor; N>1 is opt-in, and the call site is
  gated so the default path is byte-identical to main.
- Hardened config parse in agent_init (bool rejected, fractional
  floats rejected, floor 1) matching the max_attempts parser shape.
- Wired the recurring external-PR config gaps: hermes_cli/config.py
  DEFAULT_CONFIG + cli-config.yaml.example (PR only had cli.py).
- Regression tests: blank echoes / synthetic rows don't count toward
  N; tool-call/result pairs never split by the N-boundary (no-orphan
  both directions); N-guarantee wins over tail_token_budget and the
  _MAX_TAIL_MESSAGE_FLOOR (floor is a minimum, not a cap); default
  parity pin; DEFAULT_CONFIG pin.
teknium1 added a commit that referenced this pull request Jul 23, 2026
…or-preserving default

Follow-up fixes on top of the salvaged #22566 mechanism:

- N-collector now counts only REAL actionable user turns via
  _is_actionable_user_turn + _is_synthetic_compression_user_turn —
  the same filter pair _find_last_user_message_idx uses post-#69291.
  The contributor's bare role=='user' + _is_context_summary_content
  check let blank platform echoes and continuation/todo rows consume
  N slots, silently degrading the guarantee.
- Default flipped 3 -> 1 (behavior-preserving): a default of 3 was
  measured to change the tail cut on transcripts whose budget covers
  only the last turn. min_tail_user_messages=1 delegates to the
  existing single-user anchor; N>1 is opt-in, and the call site is
  gated so the default path is byte-identical to main.
- Hardened config parse in agent_init (bool rejected, fractional
  floats rejected, floor 1) matching the max_attempts parser shape.
- Wired the recurring external-PR config gaps: hermes_cli/config.py
  DEFAULT_CONFIG + cli-config.yaml.example (PR only had cli.py).
- Regression tests: blank echoes / synthetic rows don't count toward
  N; tool-call/result pairs never split by the N-boundary (no-orphan
  both directions); N-guarantee wins over tail_token_budget and the
  _MAX_TAIL_MESSAGE_FLOOR (floor is a minimum, not a cap); default
  parity pin; DEFAULT_CONFIG pin.
teknium1 added a commit that referenced this pull request Jul 23, 2026
…or-preserving default

Follow-up fixes on top of the salvaged #22566 mechanism:

- N-collector now counts only REAL actionable user turns via
  _is_actionable_user_turn + _is_synthetic_compression_user_turn —
  the same filter pair _find_last_user_message_idx uses post-#69291.
  The contributor's bare role=='user' + _is_context_summary_content
  check let blank platform echoes and continuation/todo rows consume
  N slots, silently degrading the guarantee.
- Default flipped 3 -> 1 (behavior-preserving): a default of 3 was
  measured to change the tail cut on transcripts whose budget covers
  only the last turn. min_tail_user_messages=1 delegates to the
  existing single-user anchor; N>1 is opt-in, and the call site is
  gated so the default path is byte-identical to main.
- Hardened config parse in agent_init (bool rejected, fractional
  floats rejected, floor 1) matching the max_attempts parser shape.
- Wired the recurring external-PR config gaps: hermes_cli/config.py
  DEFAULT_CONFIG + cli-config.yaml.example (PR only had cli.py).
- Regression tests: blank echoes / synthetic rows don't count toward
  N; tool-call/result pairs never split by the N-boundary (no-orphan
  both directions); N-guarantee wins over tail_token_budget and the
  _MAX_TAIL_MESSAGE_FLOOR (floor is a minimum, not a cap); default
  parity pin; DEFAULT_CONFIG pin.
teknium1 added a commit that referenced this pull request Jul 24, 2026
…or-preserving default

Follow-up fixes on top of the salvaged #22566 mechanism:

- N-collector now counts only REAL actionable user turns via
  _is_actionable_user_turn + _is_synthetic_compression_user_turn —
  the same filter pair _find_last_user_message_idx uses post-#69291.
  The contributor's bare role=='user' + _is_context_summary_content
  check let blank platform echoes and continuation/todo rows consume
  N slots, silently degrading the guarantee.
- Default flipped 3 -> 1 (behavior-preserving): a default of 3 was
  measured to change the tail cut on transcripts whose budget covers
  only the last turn. min_tail_user_messages=1 delegates to the
  existing single-user anchor; N>1 is opt-in, and the call site is
  gated so the default path is byte-identical to main.
- Hardened config parse in agent_init (bool rejected, fractional
  floats rejected, floor 1) matching the max_attempts parser shape.
- Wired the recurring external-PR config gaps: hermes_cli/config.py
  DEFAULT_CONFIG + cli-config.yaml.example (PR only had cli.py).
- Regression tests: blank echoes / synthetic rows don't count toward
  N; tool-call/result pairs never split by the N-boundary (no-orphan
  both directions); N-guarantee wins over tail_token_budget and the
  _MAX_TAIL_MESSAGE_FLOOR (floor is a minimum, not a cap); default
  parity pin; DEFAULT_CONFIG pin.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Aug 2, 2026
- thread_context: propagate_context_to_thread (fail-closed callbacks)
- windows_ssh_runtime: process_state (nonce-anchored identity)
- gateway: launchd_start (self-heal + kickstart), _wait_for_systemd_service_restart
- local env: _make_run_env (provider strip + PATH ladder),
  get_temp_dir (Windows-safe forward-slash cache)
- model_switch: _configured_provider_matches (NousResearch#45006 exact routing)
- gui_uninstall: uninstall_gui (userdata opt-out)
- credential_lifecycle: _scrub_config_yaml_mirrors (value-matched)
- main: _stop_desktop_processes_locking_build (release-scope)
- plugins_cmd: _prompt_plugin_env_vars (rich spec)
- auth: persist_nous_credentials (label embed + shared mirror)
- context_compressor: _ensure_last_n_user_messages_in_tail (NousResearch#69291)
- yuanbao: parse_json_push (Pascal+snake dual)
- session_export_md: _export_body_without_hash (verification footer)
- skill_manager_tool: _find_skill_in_other_profiles (fail-quiet)
- kanban_tools: _handle_attach_url (leaf-derived filename)
- webhook_filters: filter_matches (all/any/not ops)
- nous_subscription: ensure_nous_portal_access (coverage category)
- pet/imagegen: resolve_provider (ref-capable ladder)
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…or-preserving default

Follow-up fixes on top of the salvaged NousResearch#22566 mechanism:

- N-collector now counts only REAL actionable user turns via
  _is_actionable_user_turn + _is_synthetic_compression_user_turn —
  the same filter pair _find_last_user_message_idx uses post-NousResearch#69291.
  The contributor's bare role=='user' + _is_context_summary_content
  check let blank platform echoes and continuation/todo rows consume
  N slots, silently degrading the guarantee.
- Default flipped 3 -> 1 (behavior-preserving): a default of 3 was
  measured to change the tail cut on transcripts whose budget covers
  only the last turn. min_tail_user_messages=1 delegates to the
  existing single-user anchor; N>1 is opt-in, and the call site is
  gated so the default path is byte-identical to main.
- Hardened config parse in agent_init (bool rejected, fractional
  floats rejected, floor 1) matching the max_attempts parser shape.
- Wired the recurring external-PR config gaps: hermes_cli/config.py
  DEFAULT_CONFIG + cli-config.yaml.example (PR only had cli.py).
- Regression tests: blank echoes / synthetic rows don't count toward
  N; tool-call/result pairs never split by the N-boundary (no-orphan
  both directions); N-guarantee wins over tail_token_budget and the
  _MAX_TAIL_MESSAGE_FLOOR (floor is a minimum, not a cap); default
  parity pin; DEFAULT_CONFIG pin.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants