fix: preserve latest user objective during compaction #1

Open
clawlter wants to merge 3 commits from fix/preserve-active-user-objective into main
Owner

Summary

  • Preserve the latest real user objective as a labeled active-context scaffold when LCM compaction would otherwise omit it from the selected fresh tail.
  • Keep the scaffold synthetic: it is emitted inside the summary/scaffold block, not as a raw duplicate user turn, so restart reconciliation does not ingest it as another real user request.
  • Budget the scaffold under max_assembly_tokens and drop it if preserving it would reintroduce overflow.
  • Add regressions for stale-objective preservation, restart replay behavior, and assembly-cap budgeting.

Bug

In long gateway sessions, a user can change objectives and then the assistant may run many tool calls before LCM compacts. Because the fresh tail is message-count based, those assistant/tool messages can push the latest user request out of active context. The next post-compaction prompt can then contain old summarized goals plus mechanical tool traces, but not the current user request verbatim. In practice this can make the agent regress to an older task after a compression/session split.

A user-side workaround is to increase the fresh-tail count, but that is not a rigorous fix: tool-heavy turns can still exceed the configured window, and a larger tail increases context/cost for every session. The engine should preserve the active objective directly without weakening replay or overflow guardrails.

Fix

The change is intentionally narrow:

  1. During normal compress(), temporarily provide _assemble_context() with the original pre-compaction message window.
  2. _assemble_context() finds the newest non-synthetic user message when it is not already present in the selected tail.
  3. Instead of inserting a raw copied user turn, the anchor is rendered as a labeled scaffold in the summary block: [Current user objective preserved from compacted history].
  4. The scaffold is counted against the summary/assembly budget and is omitted if it would overflow max_assembly_tokens.
  5. Existing compacted-session restart semantics are preserved: replayed tail messages are still handled as before, while the new objective scaffold is not stored as a duplicate real user message.

Testing

  • PYTHONPATH=/opt/hermes:/opt/hermes-runtime/repos/hermes-lcm-fix-active-objective /opt/hermes-runtime/tools/mise/use-mise.sh pytest /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py::TestEngineABC::test_existing_compacted_session_restart_skips_synthetic_context_but_persists_new_tool /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py::TestEngineABC::test_existing_session_restart_persists_scaffolded_delta_message_matching_store_tail /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py::TestEngineABC::test_existing_session_restart_persists_scaffolded_delta_message_matching_store_tail_with_followup /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py::TestEngineABC::test_existing_compacted_session_restart_ignores_preserved_objective_anchor /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py::TestEngineCompress::test_compress_preserves_latest_user_request_outside_fresh_tail /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py::TestAssemblyGuardrails::test_context_anchor_is_budgeted_under_max_assembly_tokens -q
    • 6 passed
  • PYTHONPATH=/opt/hermes:/opt/hermes-runtime/repos/hermes-lcm-fix-active-objective /opt/hermes-runtime/tools/mise/use-mise.sh pytest /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py -q
    • 306 passed, 39 warnings
  • PYTHONPATH=/opt/hermes:/opt/hermes-runtime/repos/hermes-lcm-fix-active-objective /opt/hermes-runtime/tools/mise/use-mise.sh pytest /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_core.py /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_command.py -q
    • 508 passed, 39 warnings
  • git diff --check
  • PYTHONPATH=/opt/hermes:/opt/hermes-runtime/repos/hermes-lcm-fix-active-objective /opt/hermes-runtime/tools/mise/use-mise.sh pytest /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests -q
    • 525 passed, 39 warnings

Notes for upstream

This branch is published on a Forgejo fork because I cannot open the upstream GitHub PR directly from this environment. The diff is based on upstream main at b8d934b (v0.9.2).

## Summary - Preserve the latest real user objective as a labeled active-context scaffold when LCM compaction would otherwise omit it from the selected fresh tail. - Keep the scaffold synthetic: it is emitted inside the summary/scaffold block, not as a raw duplicate user turn, so restart reconciliation does not ingest it as another real user request. - Budget the scaffold under `max_assembly_tokens` and drop it if preserving it would reintroduce overflow. - Add regressions for stale-objective preservation, restart replay behavior, and assembly-cap budgeting. ## Bug In long gateway sessions, a user can change objectives and then the assistant may run many tool calls before LCM compacts. Because the fresh tail is message-count based, those assistant/tool messages can push the latest user request out of active context. The next post-compaction prompt can then contain old summarized goals plus mechanical tool traces, but not the current user request verbatim. In practice this can make the agent regress to an older task after a compression/session split. A user-side workaround is to increase the fresh-tail count, but that is not a rigorous fix: tool-heavy turns can still exceed the configured window, and a larger tail increases context/cost for every session. The engine should preserve the active objective directly without weakening replay or overflow guardrails. ## Fix The change is intentionally narrow: 1. During normal `compress()`, temporarily provide `_assemble_context()` with the original pre-compaction message window. 2. `_assemble_context()` finds the newest non-synthetic user message when it is not already present in the selected tail. 3. Instead of inserting a raw copied user turn, the anchor is rendered as a labeled scaffold in the summary block: `[Current user objective preserved from compacted history]`. 4. The scaffold is counted against the summary/assembly budget and is omitted if it would overflow `max_assembly_tokens`. 5. Existing compacted-session restart semantics are preserved: replayed tail messages are still handled as before, while the new objective scaffold is not stored as a duplicate real user message. ## Testing - `PYTHONPATH=/opt/hermes:/opt/hermes-runtime/repos/hermes-lcm-fix-active-objective /opt/hermes-runtime/tools/mise/use-mise.sh pytest /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py::TestEngineABC::test_existing_compacted_session_restart_skips_synthetic_context_but_persists_new_tool /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py::TestEngineABC::test_existing_session_restart_persists_scaffolded_delta_message_matching_store_tail /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py::TestEngineABC::test_existing_session_restart_persists_scaffolded_delta_message_matching_store_tail_with_followup /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py::TestEngineABC::test_existing_compacted_session_restart_ignores_preserved_objective_anchor /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py::TestEngineCompress::test_compress_preserves_latest_user_request_outside_fresh_tail /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py::TestAssemblyGuardrails::test_context_anchor_is_budgeted_under_max_assembly_tokens -q` - `6 passed` - `PYTHONPATH=/opt/hermes:/opt/hermes-runtime/repos/hermes-lcm-fix-active-objective /opt/hermes-runtime/tools/mise/use-mise.sh pytest /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py -q` - `306 passed, 39 warnings` - `PYTHONPATH=/opt/hermes:/opt/hermes-runtime/repos/hermes-lcm-fix-active-objective /opt/hermes-runtime/tools/mise/use-mise.sh pytest /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_engine.py /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_core.py /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests/test_lcm_command.py -q` - `508 passed, 39 warnings` - `git diff --check` - `PYTHONPATH=/opt/hermes:/opt/hermes-runtime/repos/hermes-lcm-fix-active-objective /opt/hermes-runtime/tools/mise/use-mise.sh pytest /opt/hermes-runtime/repos/hermes-lcm-fix-active-objective/tests -q` - `525 passed, 39 warnings` ## Notes for upstream This branch is published on a Forgejo fork because I cannot open the upstream GitHub PR directly from this environment. The diff is based on upstream `main` at `b8d934b` (`v0.9.2`).
fix: preserve latest user objective during compaction
All checks were successful
CI / test (3.11) (pull_request) Successful in 1m42s
CI / test (3.12) (pull_request) Successful in 1m41s
CI / test (3.13) (pull_request) Successful in 2m57s
8c409cc1e7
clawlter force-pushed fix/preserve-active-user-objective from 8c409cc1e7
All checks were successful
CI / test (3.11) (pull_request) Successful in 1m42s
CI / test (3.12) (pull_request) Successful in 1m41s
CI / test (3.13) (pull_request) Successful in 2m57s
to 51022ce4de
All checks were successful
CI / test (3.11) (pull_request) Successful in 1m43s
CI / test (3.12) (pull_request) Successful in 2m54s
CI / test (3.13) (pull_request) Successful in 1m40s
2026-05-08 21:15:48 -04:00
Compare
Author
Owner

Updated the branch after tightening the design from PR feedback: the preserved objective is now a labeled synthetic scaffold inside the summary block, budgeted under max_assembly_tokens, and covered by restart-replay/cap regressions. Local full plugin tests pass (525 passed), and Forgejo CI is green on 3.11/3.12/3.13 for head 51022ce.

Updated the branch after tightening the design from PR feedback: the preserved objective is now a labeled synthetic scaffold inside the summary block, budgeted under max_assembly_tokens, and covered by restart-replay/cap regressions. Local full plugin tests pass (525 passed), and Forgejo CI is green on 3.11/3.12/3.13 for head 51022ce.
fix: carry preserved objective through repeated compaction
All checks were successful
CI / test (3.11) (pull_request) Successful in 3m28s
CI / test (3.12) (pull_request) Successful in 1m44s
CI / test (3.13) (pull_request) Successful in 1m41s
f9ad827b36
fix: preserve objective in overflow recovery
All checks were successful
CI / test (3.11) (pull_request) Successful in 1m43s
CI / test (3.12) (pull_request) Successful in 1m43s
CI / test (3.13) (pull_request) Successful in 1m44s
3249c6b898
All checks were successful
CI / test (3.11) (pull_request) Successful in 1m43s
CI / test (3.12) (pull_request) Successful in 1m43s
CI / test (3.13) (pull_request) Successful in 1m44s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/preserve-active-user-objective:fix/preserve-active-user-objective
git switch fix/preserve-active-user-objective

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff fix/preserve-active-user-objective
git switch fix/preserve-active-user-objective
git rebase main
git switch main
git merge --ff-only fix/preserve-active-user-objective
git switch fix/preserve-active-user-objective
git rebase main
git switch main
git merge --no-ff fix/preserve-active-user-objective
git switch main
git merge --squash fix/preserve-active-user-objective
git switch main
git merge --ff-only fix/preserve-active-user-objective
git switch main
git merge fix/preserve-active-user-objective
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
clawlter/hermes-lcm!1
No description provided.