You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This fixes lifecycle plugin hooks that received different session transition and workspace payloads depending on whether /new ran through the CLI, TUI, or messaging gateway. Without this fix, plugins cannot reliably release old-session resources, initialize the replacement session, or associate model turns with the authoritative task workspace.
Symptom
An explicit /new produced incomplete or inconsistent on_session_finalize and on_session_reset keyword arguments across surfaces. pre_llm_call also omitted the authoritative task/session workspace, while some lifecycle paths could substitute process state for an unproven session cwd.
Impact
Native plugins that coordinate per-session state cannot distinguish the old and new session consistently across CLI, TUI, and Gateway. Workspace-aware plugins can attribute lifecycle events or model turns to the wrong directory, and TUI prewarming could emit reset before the old session was finalized.
Bug Cause
Trigger:cli.py::_start_new_session, ui-tui/src/app/useSessionLifecycle.ts::startNewSession, gateway/slash_commands.py::_handle_new_session, and agent/turn_context.py::build_turn_context
Causal chain:
A user starts a new session or a model turn from a surface with an authoritative session/task workspace.
Each surface constructs plugin hook payloads independently, omitting transition IDs or cwd on some paths. The TUI also created the replacement session before associating it atomically with teardown of the old session.
Plugins observe surface-dependent payloads, and TUI plugins can observe reset before finalize.
Why it is wrong: Lifecycle payloads describe one semantic session transition, so their identifiers, reason, cwd, and ordering must not depend on the client surface. A process cwd is not evidence of a session workspace.
Working sibling / contrast: Gateway already supplied old and new session IDs for explicit rotation, but it lacked authoritative cwd. CLI, TUI, and pre-LLM paths each supplied different subsets of the same context.
Ruled out: This is not a plugin callback compatibility issue. Hook dispatch already filters additive keyword fields for callbacks with narrow signatures; the failure was at the surface-specific payload construction and TUI transition ordering.
Fix
Add a shared authoritative session cwd resolver that uses recorded task/session state and returns an empty value when no cwd is proven.
Add consistent old_session_id, new_session_id, reason, and cwd fields to explicit session rotation hooks across CLI, TUI, and Gateway.
Include authoritative cwd in pre_llm_call payloads.
Associate TUI rotations atomically and defer the replacement-session reset until old-session finalization, without fabricating transitions for ordinary startup, resume, or shutdown.
gateway/run.py and gateway/slash_commands.py - propagate current session identity and authoritative cwd to model-turn and rotation hooks.
tui_gateway/methods_session.py, tui_gateway/server.py, ui-tui/src/app/useSessionLifecycle.ts, and ui-tui/src/gatewayTypes.ts - preserve old-session workspace context and enforce finalize-before-reset ordering.
tests/ and ui-tui/src/__tests__/useSessionLifecycle.test.ts - cover payload parity, unknown cwd behavior, rotation ordering, and gateway/TUI races.
How to Test
Register a native plugin that records pre_llm_call, on_session_finalize, and on_session_reset keyword arguments.
Start a session in an explicit workspace through CLI, TUI, or Gateway, send one turn, and run /new.
Confirm finalize precedes reset, both lifecycle hooks carry identical transition IDs, reason, and authoritative cwd, and ordinary startup does not invent a transition.
Run the related automated suites:
scripts/run_tests.sh tests/agent/test_runtime_cwd.py tests/agent/test_api_content_sidecar.py tests/cli/test_session_boundary_hooks.py tests/test_tui_gateway_server.py tests/gateway/test_35994_reset_button_deadlock.py
cd ui-tui && npm test -- --run src/__tests__/useSessionLifecycle.test.ts
cd ui-tui && npm run typecheck
The Python suite passed 597 tests, the targeted TUI suite passed 7 tests, TypeScript typecheck passed, and the lifecycle contract was verified with a real native plugin across CLI, TUI, Gateway, and pre-LLM paths on Windows 11.
Summary: authoritative_session_cwd (agent/runtime_cwd.py:73) falls back to the process-global TERMINAL_CWD when neither a recorded task cwd nor a session-cwd override is proven, contradicting the PR's "returns an empty value when no cwd is proven" and issue #85262's ban on a host-process fallback.
Problems:
agent/runtime_cwd.py:73 returns os.environ.get("TERMINAL_CWD", "").strip() once neither a recorded task/session cwd nor a _SESSION_CWD override exists — TERMINAL_CWD is a single process-global env var bridged once from terminal.cwd at process start, not a per-session value.
tools/terminal_tool.py:1423 documents that the process env var "outlives the session that set it," so a fresh session with no proven workspace inherits the previous session's directory — the wrong-directory attribution this PR says it prevents.
Solution:
Return "" when neither a recorded task/session cwd nor a _SESSION_CWD override is proven, instead of falling back to the process-global TERMINAL_CWD.
Evidence
no deterministic fact backs this claim — model belief, not executed or read evidence
Checked against fde5275 — the PR head when this was written — and fa83af3, main at the same moment.
Fixed the stale process-level workspace fallback in authoritative_session_cwd(). When neither a task cwd nor a session override is proven, lifecycle and pre-LLM hooks now receive an empty cwd even if TERMINAL_CWD still contains a previous session value. The regression test now sets a non-empty stale TERMINAL_CWD and verifies it is rejected.\n\nVerified with: scripts/run_tests.sh tests/agent/test_runtime_cwd.py (8 passed).
Fixed the CI failure in tests/test_lazy_session_regressions.py by updating the _notify_session_boundary test double to accept the additive lifecycle keyword context now passed by _finalize_session(). This keeps the regression focused on finalizing the agent’s current session instead of failing on the mock signature.\n\nVerified with: scripts/run_tests.sh tests/test_lazy_session_regressions.py (7 passed).
AI code review — automated review for reference, author can ignore or act on any point.
fix(plugins): keep lifecycle hooks aligned across session rotation surfaces
A missed finalize permanently suppresses on_session_reset — tui_gateway/server.py, _notify_pending_session_reset(): it is the only place boundary_reset_pending is popped and boundary_reset_emitted set, while both _build() and _init_session() skip the reset hook whenever the flag is present. If the old session's finalize never runs (old entry already removed from _sessions, or close on a path that skips _finalize_session), the new session's on_session_reset is suppressed indefinitely and the flag never clears. Suggest a fallback: if a session holds boundary_reset_pending but its boundary_old_session_id no longer exists, emit the reset directly.
cli.py cwd resolution depends on call ordering — _notify_session_boundary() resolves the finalize cwd from self.agent._current_task_id at call time. Correct today because finalize precedes the agent swap in new_session(), but any future caller firing finalize after reset would report the NEW task's cwd. Consider threading the old task id explicitly, as done for old_session_id/new_session_id.
session.create now precedes session.close in the TUI — ui-tui/src/app/useSessionLifecycle.ts: between the two RPCs both sessions are live, and if close fails (e.g. network drop) the old session stays open in its slot while the UI has already switched to the new one. Previously close happened first. Consider close-then-create with the rotation params, or retry/cleanup on close failure.
Silent downgrade when the old runtime sid is gone — tui_gateway/methods_session.py: if _sessions.get(boundary_old_session_id) is None, the rotation is silently downgraded to a plain reset (boundary_old_session_id = ""). A debug log would help diagnose lost transition events.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
area/sessionsSession lifecycle, resume, persistence, historycomp/agentCore agent runtime: loop, agent_init, prompt builder, context-compression, responses endpointcomp/cliCLI entry point, hermes_cli/, setup wizardcomp/gatewayGateway runner, session dispatch, deliverycomp/pluginsPlugin system and bundled pluginscomp/tuiTerminal UI (ui-tui/ + tui_gateway/)P3Low — cosmetic, nice to havesweeper:risk-message-deliverySweeper risk: may drop, duplicate, misroute, or suppress messagessweeper:risk-session-stateSweeper risk: may lose/corrupt/mis-associate session or context statetype/bugSomething isn't working
4 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
This fixes lifecycle plugin hooks that received different session transition and workspace payloads depending on whether
/newran through the CLI, TUI, or messaging gateway. Without this fix, plugins cannot reliably release old-session resources, initialize the replacement session, or associate model turns with the authoritative task workspace.Symptom
An explicit
/newproduced incomplete or inconsistenton_session_finalizeandon_session_resetkeyword arguments across surfaces.pre_llm_callalso omitted the authoritative task/session workspace, while some lifecycle paths could substitute process state for an unproven session cwd.Impact
Native plugins that coordinate per-session state cannot distinguish the old and new session consistently across CLI, TUI, and Gateway. Workspace-aware plugins can attribute lifecycle events or model turns to the wrong directory, and TUI prewarming could emit reset before the old session was finalized.
Bug Cause
Trigger:
cli.py::_start_new_session,ui-tui/src/app/useSessionLifecycle.ts::startNewSession,gateway/slash_commands.py::_handle_new_session, andagent/turn_context.py::build_turn_contextCausal chain:
Why it is wrong: Lifecycle payloads describe one semantic session transition, so their identifiers, reason, cwd, and ordering must not depend on the client surface. A process cwd is not evidence of a session workspace.
Working sibling / contrast: Gateway already supplied old and new session IDs for explicit rotation, but it lacked authoritative cwd. CLI, TUI, and pre-LLM paths each supplied different subsets of the same context.
Ruled out: This is not a plugin callback compatibility issue. Hook dispatch already filters additive keyword fields for callbacks with narrow signatures; the failure was at the surface-specific payload construction and TUI transition ordering.
Fix
old_session_id,new_session_id,reason, andcwdfields to explicit session rotation hooks across CLI, TUI, and Gateway.pre_llm_callpayloads.Related Issue
Closes #85262
Type of Change
Changes Made
agent/runtime_cwd.pyandagent/turn_context.py- resolve and expose authoritative session/task cwd without a process-cwd fallback.cli.py- emit complete explicit session transition payloads.gateway/run.pyandgateway/slash_commands.py- propagate current session identity and authoritative cwd to model-turn and rotation hooks.tui_gateway/methods_session.py,tui_gateway/server.py,ui-tui/src/app/useSessionLifecycle.ts, andui-tui/src/gatewayTypes.ts- preserve old-session workspace context and enforce finalize-before-reset ordering.tests/andui-tui/src/__tests__/useSessionLifecycle.test.ts- cover payload parity, unknown cwd behavior, rotation ordering, and gateway/TUI races.How to Test
pre_llm_call,on_session_finalize, andon_session_resetkeyword arguments./new.The Python suite passed 597 tests, the targeted TUI suite passed 7 tests, TypeScript typecheck passed, and the lifecycle contract was verified with a real native plugin across CLI, TUI, Gateway, and pre-LLM paths on Windows 11.
Checklist
Code
fix(scope):,feat(scope):, etc.)Documentation & Housekeeping
docs/, docstrings) - N/A, no user-facing configuration or documented API changedcli-config.yaml.exampleif I added/changed config keys - N/A, no config keys changedCONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows - N/A, the existing additive plugin hook contract is unchangedScreenshots / Logs
Not applicable. Verification results are listed in How to Test.