{"repo":"NousResearch/hermes-agent","public_id":"38727","display_label":"Compression locking, forking, and concurrency bugs","summary":"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.","subsystem":"agent","counts":{"members":13,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":4,"merged_prs":2,"attempt_prs":7},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":41708},"resolution":{"state":"partially_resolved","by":"pr-41708"},"freshness":{"state":"verified","last_verified_at":"2026-07-01T13:38:25+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.12923076923076923},"fix_edges":[{"issue_number":38727,"pr_number":41708,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":56391,"pr_number":56416,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":38727,"title":"[Bug]:  `per-session compression lock` only prevents same-window concurrent fork, but not cross-turn stale-parent fork","url":null,"state":"closed"},{"kind":"issue","number":52526,"title":"Flaky test: test_compression_concurrent_fork.py::test_concurrent_compression_does_not_fork_session","url":null,"state":"closed"},{"kind":"issue","number":56391,"title":"Interrupt-protected compression + default busy_input_mode=interrupt can fork multiple orphaned compression siblings from one stale parent during a message burst","url":null,"state":"closed"},{"kind":"issue","number":63129,"title":"[Bug]: Compression lock fails OPEN on any acquisition error — concurrent compression can fork the session","url":null,"state":"closed"},{"kind":"pr","number":38746,"title":"[codex] Disable compression in background review fork","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"a77bc2c08dfa4d999463e959a94ab8e21a3ba9f6","number":0},{"kind":"pr","value":"#41708","number":41708}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":41708,"title":"fix(compression): disable compression on background-review fork (#38727)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/background_review.py","tests/agent/test_compression_concurrent_fork.py"],"fix_hunks":[{"file":"agent/background_review.py","hunks":["@@ -449,6 +449,17 @@ def _bg_review_auto_deny(command, description, **kwargs):\n             # if a future code path bypasses the cache.\n             review_agent.session_start = agent.session_start\n             review_agent.session_id = agent.session_id\n+            # Never let the review fork compress. It shares the parent's\n+            # session_id, so if it won a compression race it would rotate the\n+            # parent into a NEW child that the gateway never adopts (the fork\n+            # is single-lifecycle and dies right after this run_conversation).\n+            # The foreground turn would then start from the stale parent and\n+            # compress it again, leaving the same parent with two sibling\n+            # children (issue #38727). Review also needs full context to\n+            # produce a good memory/skill summary — compressing would strip\n+            # detail. Both compression triggers in conversation_loop.py gate on\n+            # agent.compression_enabled, so this short-circuits both paths.\n+            review_agent.compression_enabled = False\n \n             from model_tools import get_tool_definitions\n             from hermes_cli.plugins import ("]},{"file":"tests/agent/test_compression_concurrent_fork.py","hunks":["@@ -238,3 +238,75 @@ def test_missing_lock_subsystem_fails_open_not_infinite_loop(tmp_path: Path) ->\n     )\n     # Session rotated (compression succeeded end-to-end).\n     assert agent.session_id != parent_sid\n+\n+\n+def test_review_fork_disables_compression_to_prevent_stale_parent_fork() -> None:\n+    \"\"\"The background-review fork must set ``compression_enabled = False``\n+    so it can never compress the parent it shares a session_id with\n+    (issue #38727).\n+\n+    The per-session compression lock only serialises a SAME-WINDOW concurrent\n+    race. It does NOT stop a stale parent from being compressed again in a\n+    LATER turn: if ``review_agent`` had won the race, its new child session is\n+    never adopted by the gateway (the fork is single-lifecycle and dies right\n+    after one ``run_conversation``), so the foreground path would start the\n+    next turn from the stale parent and compress it AGAIN — leaving the same\n+    parent with two sibling children.\n+\n+    The fix makes the review fork never trigger compression at all. Both\n+    compression trigger sites in ``agent/conversation_loop.py`` gate on\n+    ``agent.compression_enabled`` BEFORE calling ``_compress_context``:\n+      • preflight (``if agent.compression_enabled and len(messages) > ...``)\n+      • mid-loop  (``if agent.compression_enabled and _compressor.should_compress(...)``)\n+    so a fork with the flag cleared never reaches the rotation path.\n+\n+    This test pins the contract at the source: ``_run_review_in_thread``\n+    must set ``review_agent.compression_enabled = False`` on the fork it\n+    builds. It calls the real worker synchronously with\n+    ``AIAgent.run_conversation`` patched (so no LLM call happens) and\n+    captures the constructed review agent to assert the flag.\n+    \"\"\"\n+    import tempfile\n+\n+    import agent.background_review as br\n+\n+    captured = {}\n+\n+    def _fake_run_conversation(self, *_a, **_k):\n+        captured[\"compression_enabled\"] = self.compression_enabled\n+        captured[\"session_id\"] = self.session_id\n+        return {\"final_response\": \"\", \"messages\": []}\n+\n+    parent_sid = \"REVIEW_FORK_FLAG_TEST\"\n+\n+    with tempfile.TemporaryDirectory() as td:\n+        db = SessionDB(db_path=Path(td) / \"state.db\")\n+        db.create_session(parent_sid, source=\"discord\")\n+        parent = _build_agent_with_db(db, parent_sid)\n+\n+        # The worker does a local ``from run_agent import AIAgent``; patching\n+        # the class method covers that import path.\n+        from run_agent import AIAgent\n+\n+        with patch.object(AIAgent, \"run_conversation\", _fake_run_conversation):\n+            br._run_review_in_thread(\n+                parent,\n+                [{\"role\": \"user\", \"content\": \"hi\"}],\n+                \"review this conversation\",\n+            )\n+\n+    assert captured, (\n+        \"_run_review_in_thread never reached run_conversation — the spawn path \"\n+        \"changed; update this test to capture the review AIAgent.\"\n+    )\n+    assert captured[\"session_id\"] == parent_sid, (\n+        \"Review fork should inherit the parent's session_id (shared id is the \"\n+        \"whole reason compression must be disabled).\"\n+    )\n+    assert captured[\"compression_enabled\"] is False, (\n+        \"FIX REGRESSION: background-review fork did NOT disable compression. \"\n+        \"It shares the parent's session_id, so an enabled fork can rotate the \"\n+        \"parent into an orphan child (issue #38727). The trigger gates in \"\n+        \"conversation_loop.py only short-circuit when compression_enabled is \"\n+        \"False — this flag MUST be cleared on the review fork.\"\n+    )\n"]}]},{"kind":"pr","number":52547,"title":"fix(test): barrier eliminates race in compression fork test (#52526)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d2d470e3217a42b22f56b60d1675c1e8917ea030","number":0},{"kind":"commit","value":"d2d470e3217a42b22f56b60d1675c1e8917ea030","number":0},{"kind":"pr","value":"#52526","number":52526}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":56404,"title":"fix(gateway): queue interrupts during in-flight context compression","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#56416","number":56416},{"kind":"pr","value":"#56391","number":56391}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":56410,"title":"fix(gateway): wait for in-flight compression before enqueuing interrupted messages","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#56391","number":56391},{"kind":"pr","value":"#56391","number":56391},{"kind":"pr","value":"#56416","number":56416},{"kind":"pr","value":"#56391","number":56391},{"kind":"pr","value":"#56391","number":56391},{"kind":"pr","value":"#56442","number":56442}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":56414,"title":"fix(gateway): stop compression sibling forks from message bursts (salvage #56404, fixes #56391)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"5eaccf580","number":0},{"kind":"pr","value":"#56391","number":56391},{"kind":"pr","value":"#56404","number":56404}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":56416,"title":"fix(gateway): queue interrupts during in-flight context compression (#56391)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/run.py","scripts/release.py","tests/gateway/test_compression_interrupt_demotion_56391.py"]},{"kind":"pr","number":63132,"title":"fix(agent): fail closed on unexpected compression-lock acquisition errors","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":64183,"title":"fix(compression): fail CLOSED on transient lock errors, OPEN on structural absence (#63129)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"2627933f3","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true}],"trust":{"default_tier":"verified","includes_suggested":false}}