# NousResearch/hermes-agent · Compression locking, forking, and concurrency bugs

## Open now

- Nothing open on GitHub.

These items concern bugs and fixes around concurrent context compression: lock failure modes (fail-open vs. fail-closed), session forking from concurrent or cross-turn compression, and interrupt handling during in-flight compression. Several test reliability issues related to these race conditions are also included.

## TL;DR

- 13 member(s): 0 open issue(s), 4 closed issue(s), 0 open PR(s), 2 merged PR(s), 7 PR(s) closed without merging.
- A fix was merged to main in PR #41708. Verify it addresses your case.
- **Recommended:** read PR #41708 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-41708`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.13`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #38727: \[Bug\]:  \`per-session compression lock\` only prevents same-window concurrent fork, but not cross-turn stale-parent fork
- #52526: Flaky test: test_compression_concurrent_fork.py::test_concurrent_compression_does_not_fork_session
- #56391: Interrupt-protected compression + default busy_input_mode=interrupt can fork multiple orphaned compression siblings from one stale parent during a message burst
- #63129: \[Bug\]: Compression lock fails OPEN on any acquisition error — concurrent compression can fork the session

## Prior & competing PRs

- **#41708: fix(compression): disable compression on background-review fork (#38727)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/background_review.py`, `tests/agent/test_compression_concurrent_fork.py`
  - fix hunks:
  - `agent/background_review.py`

    ```diff
    @@ -449,6 +449,17 @@ def _bg_review_auto_deny(command, description, **kwargs):
                 # if a future code path bypasses the cache.
                 review_agent.session_start = agent.session_start
                 review_agent.session_id = agent.session_id
    +            # Never let the review fork compress. It shares the parent's
    +            # session_id, so if it won a compression race it would rotate the
    +            # parent into a NEW child that the gateway never adopts (the fork
    +            # is single-lifecycle and dies right after this run_conversation).
    +            # The foreground turn would then start from the stale parent and
    +            # compress it again, leaving the same parent with two sibling
    +            # children (issue #38727). Review also needs full context to
    +            # produce a good memory/skill summary — compressing would strip
    +            # detail. Both compression triggers in conversation_loop.py gate on
    +            # agent.compression_enabled, so this short-circuits both paths.
    +            review_agent.compression_enabled = False
     
                 from model_tools import get_tool_definitions
                 from hermes_cli.plugins import (
    ```
  - `tests/agent/test_compression_concurrent_fork.py`

    ```diff
    @@ -238,3 +238,75 @@ def test_missing_lock_subsystem_fails_open_not_infinite_loop(tmp_path: Path) ->
         )
         # Session rotated (compression succeeded end-to-end).
         assert agent.session_id != parent_sid
    +
    +
    +def test_review_fork_disables_compression_to_prevent_stale_parent_fork() -> None:
    +    """The background-review fork must set ``compression_enabled = False``
    +    so it can never compress the parent it shares a session_id with
    +    (issue #38727).
    +
    +    The per-session compression lock only serialises a SAME-WINDOW concurrent
    +    race. It does NOT stop a stale parent from being compressed again in a
    +    LATER turn: if ``review_agent`` had won the race, its new child session is
    +    never adopted by the gateway (the fork is single-lifecycle and dies right
    +    after one ``run_conversation``), so the foreground path would start the
    +    next turn from the stale parent and compress it AGAIN — leaving the same
    +    parent with two sibling children.
    +
    +    The fix makes the review fork never trigger compression at all. Both
    +    compression trigger sites in ``agent/conversation_loop.py`` gate on
    +    ``agent.compression_enabled`` BEFORE calling ``_compress_context``:
    +      • preflight (``if agent.compression_enabled and len(messages) > ...``)
    +      • mid-loop  (``if agent.compression_enabled and _compressor.should_compress(...)``)
    +    so a fork with the flag cleared never reaches the rotation path.
    +
    +    This test pins the contract at the source: ``_run_review_in_thread``
    +    must set ``review_agent.compression_enabled = False`` on the fork it
    +    builds. It calls the real worker synchronously with
    +    ``AIAgent.run_conversation`` patched (so no LLM call happens) and
    +    captures the constructed review agent to assert the flag.
    +    """
    +    import tempfile
    +
    +    import agent.background_review as br
    +
    +    captured = {}
    +
    +    def _fake_run_conversation(self, *_a, **_k):
    +        captured["compression_enabled"] = self.compression_enabled
    +        captured["session_id"] = self.session_id
    +        return {"final_response": "", "messages": []}
    +
    +    parent_sid = "REVIEW_FORK_FLAG_TEST"
    +
    +    with tempfile.TemporaryDirectory() as td:
    +        db = SessionDB(db_path=Path(td) / "state.db")
    +        db.create_session(parent_sid, source="discord")
    +        parent = _build_agent_with_db(db, parent_sid)
    +
    +        # The worker does a local ``from run_agent import AIAgent``; patching
    +        # the class method covers that import path.
    +        from run_agent import AIAgent
    +
    +        with patch.object(AIAgent, "run_conversation", _fake_run_conversation):
    +            br._run_review_in_thread(
    +                parent,
    +                [{"role": "user", "content": "hi"}],
    +                "review this conversation",
    +            )
    +
    +    assert captured, (
    +        "_run_review_in_thread never reached run_conversation — the spawn path "
    +        "changed; update this test to capture the review AIAgent."
    +    )
    +    assert captured["session_id"] == parent_sid, (
    +        "Review fork should inherit the parent's session_id (shared id is the "
    +        "whole reason compression must be disabled)."
    +    )
    +    assert captured["compression_enabled"] is False, (
    +        "FIX REGRESSION: background-review fork did NOT disable compression. "
    +        "It shares the parent's session_id, so an enabled fork can rotate the "
    +        "parent into an orphan child (issue #38727). The trigger gates in "
    +        "conversation_loop.py only short-circuit when compression_enabled is "
    +        "False — this flag MUST be cleared on the review fork."
    +    )
    ```
- **#52547: fix(test): barrier eliminates race in compression fork test (#52526)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `d2d470e3217a42b22f56b60d1675c1e8917ea030`, `d2d470e3217a42b22f56b60d1675c1e8917ea030`, `#52526`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#56416: fix(gateway): queue interrupts during in-flight context compression (#56391)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/run.py`, `scripts/release.py`, `tests/gateway/test_compression_interrupt_demotion_56391.py`
- **#64183: fix(compression): fail CLOSED on transient lock errors, OPEN on structural absence (#63129)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `2627933f3`
- **#38746: \[codex\] Disable compression in background review fork**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `a77bc2c08dfa4d999463e959a94ab8e21a3ba9f6`, `#41708`
- **#56404: fix(gateway): queue interrupts during in-flight context compression**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#56416`, `#56391`
- **#56410: fix(gateway): wait for in-flight compression before enqueuing interrupted messages**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#56391`, `#56391`, `#56416`, `#56391`, `#56391`, `#56442`
- **#56414: fix(gateway): stop compression sibling forks from message bursts (salvage #56404, fixes #56391)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `5eaccf580`, `#56391`, `#56404`
- **#63132: fix(agent): fail closed on unexpected compression-lock acquisition errors**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`

## Fix status

- Merged to main in PR #41708 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-01`.
- Subsystem: `agent`
