Skip to content

fix(plugins): keep lifecycle hooks aligned across session rotation surfaces - #85314

Open
fangliquanflq wants to merge 5 commits into
NousResearch:mainfrom
fangliquanflq:fix/plugin-session-lifecycle-payloads
Open

fangliquanflq wants to merge 5 commits into
NousResearch:mainfrom
fangliquanflq:fix/plugin-session-lifecycle-payloads

Conversation

@fangliquanflq

Copy link
Copy Markdown

What does this PR do?

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:

  1. A user starts a new session or a model turn from a surface with an authoritative session/task workspace.
  2. 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.
  3. 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.

Related Issue

Closes #85262

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Changes Made

  • agent/runtime_cwd.py and agent/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.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

  1. Register a native plugin that records pre_llm_call, on_session_finalize, and on_session_reset keyword arguments.
  2. Start a session in an explicit workspace through CLI, TUI, or Gateway, send one turn, and run /new.
  3. Confirm finalize precedes reset, both lifecycle hooks carry identical transition IDs, reason, and authoritative cwd, and ordinary startup does not invent a transition.
  4. 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.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run the repository test entry on the relevant suites and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Windows 11

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) - N/A, no user-facing configuration or documented API changed
  • I've updated cli-config.yaml.example if I added/changed config keys - N/A, no config keys changed
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows - N/A, the existing additive plugin hook contract is unchanged
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide
  • I've updated tool descriptions/schemas if I changed tool behavior - N/A, no model tool behavior changed

Screenshots / Logs

Not applicable. Verification results are listed in How to Test.

@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/plugins Plugin system and bundled plugins area/sessions Session lifecycle, resume, persistence, history P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 13, 2026
@spfcraze

Copy link
Copy Markdown

This was generated by AI during triage.

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.

@fangliquanflq

Copy link
Copy Markdown
Author

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).

@fangliquanflq

Copy link
Copy Markdown
Author

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).

@Enough1122

Copy link
Copy Markdown

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

@alt-glitch alt-glitch added the sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages label Aug 16, 2026

This branch has not been deployed

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

Labels

area/sessions Session lifecycle, resume, persistence, history comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Session lifecycle plugin payloads lack cross-surface transition and workspace parity

4 participants