A compression attempt that ends by host timeout, cooldown/backoff block, or gateway
hygiene turn-hold writes its terminal activity stamp through _touch_activity without
force_persist. The durable SessionDB activity projection is rate-limited to one write
per SESSION_ACTIVITY_HEARTBEAT_MIN_INTERVAL_SECONDS (60s), and the compression
heartbeat had just written 'context compression in progress' — so the terminal stamp
almost always lands inside the open window and is dropped.
Nothing writes after it: the turn is over, the worker is detached, the host may be
gone. sessions.last_activity_description stays 'context compression in progress' with
last_activity_provenance 'agent.compression' forever, next to a
compression_failure_error and a cooldown timestamp that prove the attempt terminated.
Every surface reading the durable row (chat lists, session listings, reconnect,
orphan reap, stall watchdog) shows a permanently stuck/compressing chat with no turn
running.
Fix at the single write path: _touch_activity bypasses the persist rate limit for any
terminal compression provenance, exactly as force_persist does. The rate limit itself
is unchanged for mid-compression heartbeats and every other writer, so no extra write
pressure is added to the contended SessionDB path — one extra write per terminated
compression attempt.
TERMINAL_COMPRESSION_PROVENANCES is deliberately separate from
conversation_compression._TERMINAL_COMPRESSION_PROVENANCES: that set answers 'may a
detached heartbeat overwrite this stamp?' and excludes TURNHOLD because the worker may
still be alive and adoptable. This set answers 'must this reach durable state now?',
which is true for turn-hold too.
`scripts/run_tests.sh tests/<dir>/` is how a change gets its regression
coverage run, so a test filed under the wrong directory is a test nobody
runs when that code changes. Two kinds of drift had accumulated.
Parallel directories for one source package, folded into the mirror:
tests/acp -> tests/acp_adapter (its __init__/conftest move with it)
tests/cli -> tests/hermes_cli (prompt_toolkit fixture merged into
hermes_cli/conftest.py)
tests/run_agent -> tests/agent (backoff fixture becomes
agent/conftest.py)
tests/relay -> tests/gateway/relay
tests/state -> tests/hermes_state
246 loose files at tests/ root, routed by the package they import/patch:
hermes_cli, hermes_state, agent, gateway, tools, plugins, tui_gateway, cron.
Installer and desktop-update script tests go to tests/scripts/{install,
desktop_update}/. 43 tests of root-level modules (batch_runner, utils,
hermes_constants, packaging) stay at the root.
Filenames drop their issue numbers (95 files: test_89315_x.py -> test_x.py);
the number stays in the module docstring where it has context.
Collisions: test_cli_skin_integration.py existed in both tests/ and tests/cli
with different subsets — merged into one (10 tests, all kept);
run_agent/test_pre_compress_memory_context.py -> agent/..._handoff.py;
tests/test_account_usage.py -> agent/test_account_usage_fetch.py;
tests/test_web_server.py -> hermes_cli/test_web_server_ws_ping.py.
Deleted: test_minisweagent_path.py (empty since PR #2804),
test_model_picker_scroll.py (tested a private copy of the logic, imported
nothing), test_process_loop_event_loop_warning.py (asserted asyncio behaviour,
imported nothing from Hermes).
Repo-root path arithmetic (Path(__file__).parents[N], dirname chains) is
bumped for the 202 files that changed depth and verified by evaluating every
such expression against the new location. classify_changes' desktop-updater
lane prefix, tests-os.yml's ignore glob and every in-tree path comment follow
the moves. tests/test_tests_tree_layout.py keeps the tree from drifting back.
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.