Compression locking, forking, and concurrency bugs
NousResearch/hermes-agent · cluster 38727 · agent
Data as of
Nothing open on GitHub. Fix merged in #41708. Check it covers your case.
The short answer
- Known?
- Yes. 13 related items grouped here, 4 issues already closed.
- Fix event
- A fix was merged to main in PR #41708. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #41708. That is separate from the fix event. - Still open
- 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.
Open issues0
Open PRs0
Fixes on record
-
#41708 fix(compression): disable compression on background-review fork (#38727)
files:
agent/background_review.py,tests/agent/test_compression_concurrent_fork.pyShow the diff
-
agent/background_review.py@@ -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@@ -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." + )
-
-
#56416 fix(gateway): queue interrupts during in-flight context compression (#56391)
files:
gateway/run.py,scripts/release.py,tests/gateway/test_compression_interrupt_demotion_56391.py
7 PRs closed without merging
From the close comments: done elsewhere 4 · merged via another change 2 · closed without comment 1
-
#38746 [codex] Disable compression in background review fork · done elsewhere →
a77bc2c,#41708 -
#52547 fix(test): barrier eliminates race in compression fork test (#52526) · done elsewhere →
d2d470e,#52526 -
#56404 fix(gateway): queue interrupts during in-flight context compression · merged via another change →
#56416,#56391 -
#56410 fix(gateway): wait for in-flight compression before enqueuing interrupted messages · merged via another change →
#56391,#56416,#56442 -
#56414 fix(gateway): stop compression sibling forks from message bursts (salvage #56404, fixes #56391) · done elsewhere →
5eaccf5,#56391,#56404
Closed issues · 4
- Closed#38727[Bug]: `per-session compression lock` only prevents same-window concurrent fork, but not cross-turn stale-parent fork
- Closed#52526Flaky test: test_compression_concurrent_fork.py::test_concurrent_compression_does_not_fork_session
- Closed#56391Interrupt-protected compression + default busy_input_mode=interrupt can fork multiple orphaned compression siblings from one stale parent during a message burst
- Closed#63129[Bug]: Compression lock fails OPEN on any acquisition error — concurrent compression can fork the session
How this cluster was grouped
-
Link evidence (
partly structural links): 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 item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab