Skip to content

fix(tui_gateway): manual /compress keeps the session workspace in the stored prompt, so the next resume hits the prompt cache - #120158

Merged
teknium1 merged 4 commits into
mainfrom
fix/tui-compress-session-cwd
Sep 23, 2026
Merged

teknium1 merged 4 commits into
mainfrom
fix/tui-compress-session-cwd

Conversation

@teknium1

@teknium1 teknium1 commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

A manual /compress in the TUI/Desktop no longer persists a system prompt pointing at the backend's home directory, so the next resume reuses it instead of rebuilding it and missing the prompt cache a second time.

  • tui_gateway/session_compression.py::_compress_session_history binds the session context and cwd (_set_session_context(key, cwd=_session_cwd(session))) around compress_now, then clears it. This matches the other RPC-thread sites that persist or inspect the prompt (server.py::_persist_live_session_system_prompt, methods_slash.py, model_switch.py).
  • All three manual-compress routes go through this one function: the session.compress RPC, the command.dispatch compress branch, and the slash.exec mirror.
  • New invariant test tests/tui_gateway/test_compress_session_cwd.py. The work done at the compaction boundary must resolve the session workspace, and the binding must not leak past the call. It is red on origin/main (it resolves the process cwd) and green with the fix.

Root cause: session.compress runs on the RPC thread, which has no session context. So the system-prompt rebuild at the compaction boundary resolved the backend's cwd (the process HOME) and stored it. Every later process (a gateway resume or hermes chat --resume) rejected that stored prompt as stale runtime and rebuilt it. That caused a second full cache miss right after the sanctioned compaction miss, and on the oneshot path the tools pin was skipped too.

Live repro: E2E tests/e2e/core/history/test_prefix_stability.py -k tui_gateway_restarts drives real python -m tui_gateway.entry processes, one durable session, with the cwd flipping between hops.

  • before (origin/main 07646a7): request 9 (in hop 4 (gw in work-b)): messages[0] (system) changed; first diverging byte at 6382: '…/home\nScratch directory…' vs '…/work-a\nScratch directory…', 1 failed
  • after (this branch): 1 passed. The only prefix break is the sanctioned compaction boundary.

The E2E file isn't part of this PR. It lands separately with the core E2E-suite PR. I copied it in temporarily for the red/green run above.

check result
test_compress_session_cwd.py on origin/main failed (/…/test_manual_compress_runs_unde0 != /…/workspace)
test_compress_session_cwd.py with fix passed
scripts/run_tests.sh tests/tui_gateway/ 1395 passed, 0 failed. test_tui_gateway_server.py hit the per-file 300s cap at load ~90, and re-run alone (with both compress files) it gave 627 passed. One unrelated timing flake in test_hosted_room_driver_runtime.py passed on retry
ruff check, check_no_tmp_literals.py, git diff --check clean

Duplicate sweep: no open or closed PR binds the session cwd around manual compression. I searched compress cwd, session.compress, compress_now session context, compress system prompt cwd stale, compress cache miss resume, _compress_session_history, session cwd compress tui and stale runtime system prompt rebuild.

Infographic

/compress keeps the workspace

session.compress runs compress_now on the RPC thread without the session context, so the
compaction-boundary system-prompt rebuild resolved the backend's cwd (the process HOME) and
persisted a prompt whose cwd line every later process rejects as stale runtime. The next
resume (TUI or 'hermes chat --resume') then rebuilt the prompt from scratch - a second
prompt-cache miss right after the sanctioned compaction one - and, on the oneshot path,
skipped the tools pin. Bind the session cwd exactly like the sibling prompt-persist sites
(server.py _persist_live_session_system_prompt, methods_slash.py).

Exposed by tests/e2e/core/history/test_prefix_stability.py[tui_gateway_restarts].
Invariant for the previous commit: the compaction-boundary work inside
_compress_session_history resolves the session's workspace, not the backend
process cwd, and the binding does not leak past the call. Red on origin/main.
@alt-glitch alt-glitch added type/bug Something isn't working P0 Critical — data loss, security, crash loop comp/tui Terminal UI (ui-tui/ + tui_gateway/) sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) labels Sep 23, 2026
@teknium1 teknium1 added the ci-reviewed applied to manually approve dangerous changes label Sep 23, 2026
@teknium1
teknium1 merged commit 89e6358 into main Sep 23, 2026
34 checks passed
@teknium1
teknium1 deleted the fix/tui-compress-session-cwd branch September 23, 2026 12:39
@teknium1

Copy link
Copy Markdown
Collaborator Author

PR Review — #120158 (P0 monitor pass)

Premise (verified): tui_gateway/session_compression.py::_compress_session_history ran compress_now on the RPC thread with no session context; the compaction-boundary system-prompt rebuild resolves cwd via agent.runtime_cwd.resolve_agent_cwd(), which fell through to the process cwd. The session.compress RPC (methods_session.py::_compress_live) binds nothing before calling it; slash.exec binds the key but not the cwd. Sibling RPC-thread sites already bind exactly this way: methods_slash.py:158, model_switch.py:383, server.py:1104, methods_connectors.py:231.

Change: 6 LOC, one try/finally around compress_now with _set_session_context(key, cwd=_session_cwd(session)) / _clear_session_context. ContextVar tokens, so the nested bind inside slash.exec restores the outer values on exit. All three manual-compress routes go through this one function.

Tests: tests/tui_gateway/test_compress_session_cwd.py red on origin/main 33a30fd (resolve_agent_cwd() = process cwd), green with the branch's session_compression.py; with it, test_compress_lock_skip, test_compression_config_hot_reload, test_compute_host_late_compress_ack, tests/agent/test_conversation_compression_manual.py: 41 passed. The test is a real invariant (cwd seen by the compressor + no leak past the call), not a change-detector.

Non-blocking: the nested bind passes no ui_session_id, so it reads "" for the duration of the summary call and is restored afterwards; nothing on the compress path consumes it today. If a later change routes an approval or emit through it, pass ui_session_id through from _compress_live as methods_connectors.py:231 does.

Looks good: fixed at the shared choke point, matches the established pattern, cache impact measured end to end (E2E prefix-stability run in the body). Merge decision stays with the maintainer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-reviewed applied to manually approve dangerous changes comp/tui Terminal UI (ui-tui/ + tui_gateway/) P0 Critical — data loss, security, crash loop sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants