Skip to content

Fix Hermes session restore: treat per-turn session-end as a turn boundary#5009

Merged
austinywang merged 5 commits into
mainfrom
issue-5000-hermes-session-restore
May 31, 2026
Merged

Fix Hermes session restore: treat per-turn session-end as a turn boundary#5009
austinywang merged 5 commits into
mainfrom
issue-5000-hermes-session-restore

Conversation

@austinywang
Copy link
Copy Markdown
Contributor

@austinywang austinywang commented May 30, 2026

Problem

Hermes loses its restore record after the first conversation turn, so quitting and relaunching cmux with a live Hermes session restores nothing.

Root cause. Hermes fires the on_session_end plugin hook once per conversation turn (at the end of every run_conversation()), not at the session boundary. The true session boundary is the separate on_session_finalize hook. In CLI/CMUXCLI+AgentHookDefinitions.swift both on_session_end and on_session_finalize map to the same session-end subcommand → .sessionEnd action.

In CLI/cmux.swift's .sessionEnd handler, a non-destructive turn-boundary path already existed (store.recordPromptStop(...)), but it was gated by a hardcoded def.name == "grok" || def.name == "antigravity" check. hermes-agent fell through to the destructive branch — store.consume(...) (which does state.sessions.removeValue(forKey:)) plus clearAgentSurfaceResumeBinding(...) — destroying the restore record and resume binding after the first turn.

Fix

Replace the hardcoded name check with a declared sessionEndIsTurnBoundary: Bool property on AgentHookDef (alongside the existing publishesStopNotification / createConfigDirIfMissing flags), set true for grok, antigravity, and hermes-agent. The .sessionEnd handler now branches on def.sessionEndIsTurnBoundary.

This turns an implicit, drift-prone name registry into a typed invariant on the definition (per the repo's "no parallel hand-maintained registries" discipline) and fixes the entire class of "per-turn session-end destroys the restore record" bugs for any restorable agent that opts in — not just hermes. grok and antigravity behavior is byte-for-byte unchanged (they now read the flag instead of their name).

Structural follow-up (not in this PR)

The deeper structural option is to split the shared mapping so on_session_end always routes through the turn-boundary path and only on_session_finalize triggers destruction. That requires a new subcommand string + action and re-installing the hook contract in users' ~/.hermes/config.yaml, with a regression risk if on_session_finalize doesn't fire reliably — a larger change than is safe to land here. The declared-flag fix is the minimal, correct, low-risk version and matches how grok/antigravity already behave (their restore records also persist across the session-end hook). Left as a follow-up.

Test

Two-commit red/green structure:

  1. Failing test — testHermesAgentSessionEndIsTurnBoundaryAndPreservesRestoreRecord in cmuxTests/CLIGenericHookPersistenceTests.swift, extending the existing grok/antigravity keep-list suite. It drives the real CLI (hooks hermes-agent session-startagent-responsesession-end) against a mock socket and asserts the per-turn session-end:
    • emits feed telemetry (turn boundary still reports),
    • does not issue clear_agent_pid hermes-agent.…,
    • does not send surface.resume.clear,
    • leaves the session record present in hermes-agent-hook-sessions.json (not consumed).
  2. The fix — turns the test green.

Reproduction note: the user-visible bug surfaces only across a quit/relaunch with a multi-turn live Hermes session, which is not practical to script locally. The unit test pins the exact handler behavior (turn-boundary path, no consume/clear) that the relaunch restore depends on.

Fixes #5000


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Changes hook routing and session store lifecycle for Hermes and restorable agents; incorrect finalize firing could leave stale state or skip cleanup, but behavior for grok/antigravity is intended to be equivalent and covered by new tests.

Overview
Fixes Hermes (and similar agents) losing session restore after the first turn by treating per-turn session-end as a turn boundary instead of full teardown.

Adds sessionEndIsTurnBoundary on AgentHookDef (enabled for grok, antigravity, hermes-agent) so .sessionEnd uses recordPromptStop and does not consume the hook session or clear resume/PID routing. Maps Hermes on_session_finalize to a new session-finalize subcommand and performAgentSessionTeardown() for real cleanup (consume store, surface.resume.clear, clear_agent_pid). Replaces the hardcoded grok/antigravity name check in the handler.

Adds integration test testHermesAgentSessionEndIsTurnBoundaryButFinalizeTearsDown covering per-turn preservation vs finalize teardown.

Reviewed by Cursor Bugbot for commit 7c82253. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fix Hermes session restore by treating per-turn on_session_end as a turn boundary and routing on_session_finalize to a dedicated teardown. Sessions now persist across quit/relaunch and finalize cleans up correctly. Fixes #5000.

  • Bug Fixes
    • Added sessionEndIsTurnBoundary to AgentHookDef; enabled for grok, antigravity, hermes-agent.
    • Introduced session-finalize subcommand and AgentHookAction.sessionFinalize; mapped Hermes on_session_finalize there.
    • Extracted shared performAgentSessionTeardown() for non–turn-boundary .sessionEnd and .sessionFinalize.
    • Added testHermesAgentSessionEndIsTurnBoundaryButFinalizeTearsDown to verify per-turn preservation and teardown on finalize.

Written for commit 7c82253. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Configurable session-end behavior so an agent’s end-of-turn can be treated as a non‑destructive turn boundary.
    • New "session finalize" action to perform true teardown and cleanup when needed.
  • Tests

    • Added integration tests verifying that per‑turn session-end preserves agent state when opted in and that the separate finalize action performs complete teardown and state removal.

Review Change Stack

austinywang and others added 2 commits May 29, 2026 22:00
…e record

Hermes fires on_session_end once per conversation turn, not at the true
session boundary. cmux maps it to the destructive session-end action, so
after the first turn the restore record and surface resume binding are
consumed and nothing survives a quit/relaunch.

This test asserts the per-turn session-end routes through the
non-destructive turn-boundary path (recordPromptStop) and does not call
store.consume / clearAgentSurfaceResumeBinding. It fails on current code.

Refs #5000

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hermes fires the on_session_end plugin hook once per conversation turn
(end of every run_conversation()), not at the true session boundary. cmux
maps both on_session_end and on_session_finalize to the destructive
session-end action, and the .sessionEnd handler only spared grok and
antigravity from consuming the session via a hardcoded name check. Because
hermes-agent was not in that list, the restore record and surface resume
binding were destroyed after the first turn, so a quit/relaunch had
nothing to restore.

Replace the hardcoded name check with a declared sessionEndIsTurnBoundary
property on AgentHookDef (alongside publishesStopNotification), set true
for grok, antigravity, and hermes-agent. This turns an implicit, drift-prone
name registry into a typed invariant on the definition and fixes the entire
class of 'per-turn session-end destroys the restore record' bugs for any
restorable agent that opts in. grok and antigravity behavior is unchanged.

Fixes #5000

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 30, 2026 11:05pm
cmux-staging Building Building Preview, Comment May 30, 2026 11:05pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 04efb93b-6d52-4259-b276-03424a002fb8

📥 Commits

Reviewing files that changed from the base of the PR and between d94357f and 7c82253.

📒 Files selected for processing (1)
  • CLI/cmux.swift

📝 Walkthrough

Walkthrough

Adds AgentHookDef.sessionEndIsTurnBoundary and AgentHookAction.sessionFinalize, sets the flag for grok/antigravity/hermes-agent, updates cmux to use the flag and a unified teardown helper, and adds a Hermes integration test distinguishing per-turn session-end from session-finalize teardown.

Changes

Session-End Turn Boundary Refactoring

Layer / File(s) Summary
Agent definition schema and configuration
CLI/CMUXCLI+AgentHookDefinitions.swift
AgentHookDef adds sessionEndIsTurnBoundary: Bool (init param + stored field), AgentHookAction adds .sessionFinalize, subcommandActions maps "session-finalize", and grok, antigravity, and hermes-agent set the flag true (hermes also routes on_session_finalize to the subcommand).
cmux teardown helper and dispatch change
CLI/cmux.swift
Adds local performAgentSessionTeardown() for destructive cleanup, switches session-end dispatch to check def.sessionEndIsTurnBoundary, and routes non-turn-boundary session-end into the .sessionFinalize teardown path.
Hermes session-end vs finalize integration test
cmuxTests/CLIGenericHookPersistenceTests.swift
New test testHermesAgentSessionEndIsTurnBoundaryButFinalizeTearsDown asserts per-turn session-end preserves Hermes restore record, routing, and resume binding while session-finalize performs destructive cleanup and removes the restore record.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐇 A tiny flag hops to say,
Turn or teardown — choose the way,
Hermes holds a patient trace,
Then finalize clears every place,
Tests sing tidy hops today.


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Algorithmic Complexity ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (16 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix: treating Hermes' per-turn session-end as a turn boundary to preserve session restore.
Description check ✅ Passed The description covers Problem, Root cause, Fix, and Test sections. However, it lacks a Testing section showing how the author tested locally and what they verified.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed AgentHookDef is an immutable struct; new sessionEndIsTurnBoundary Bool field is read-only with no actor isolation, sendability, or MainActor concerns introduced.
Cmux Swift Blocking Runtime ✅ Passed PR refactors existing session-end handling without introducing new blocking primitives; all blocking operations (flock, NSLock, semaphores) found are pre-existing code.
Cmux No Hacky Sleeps ✅ Passed Check applies only to TypeScript, JavaScript, shell, or build/runtime scripts; Swift is explicitly out of scope. This PR modifies only Swift files (.swift), so the check is not applicable.
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns: all changes are synchronous (Bool property addition, synchronous switch cases, synchronous helper function, and XCTest with runProcess/wait patterns).
Cmux Swift @Concurrent ✅ Passed No new async functions, @concurrent annotations, or nonisolated async declarations. All changes are synchronous: added property, local helper function, and test function.
Cmux Swift File And Package Boundaries ✅ Passed PR adds only +9 net lines to CLI/cmux.swift (well under 250-line threshold), introduces no mixed responsibilities, and matches the focused bug fix exception in rules.
Cmux Swift Logging ✅ Passed All logging additions comply with rules: new debug log guarded by #if DEBUG, print("{}") is CLI output, test has no logging, string matches are shell command literals.
Cmux User-Facing Error Privacy ✅ Passed PR adds no user-facing error messages, alerts, or command output. All strings are internal event names, configuration, or telemetry logging—not user-visible text.
Cmux Full Internationalization ✅ Passed PR adds internal configuration flag sessionEndIsTurnBoundary and refactors session-end logic; no user-facing text, UI strings, command names, or localization changes added.
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI code. All three modified files are CLI-related (hook definitions, command handling, tests) with zero SwiftUI patterns, imports, or state management violations.
Cmux Architecture Rethink ✅ Passed Small correctness fix replacing hardcoded agent-name checks with typed boolean configuration, with clear documented invariant, single source of truth, and no architectural violations introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR modifies agent session lifecycle handling and adds a test; no window-related code (NSWindow, NSPanel, NSWindowController, SwiftUI Window, WindowGroup) is added or modified.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-5000-hermes-session-restore

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 30, 2026

Greptile Summary

This PR fixes the Hermes session restore regression (#5000) by correctly separating the per-turn on_session_end hook (non-destructive turn boundary) from the true session teardown on_session_finalize hook. It replaces a hardcoded grok/antigravity name check with a typed sessionEndIsTurnBoundary property on AgentHookDef, and adds a dedicated session-finalize subcommand/action for genuine destructive cleanup.

  • AgentHookDef.sessionEndIsTurnBoundary (default false) replaces the name-registry pattern; enabled for grok, antigravity, and hermes-agent. The .sessionEnd handler now reads this flag instead of comparing strings.
  • on_session_finalize routing fixed: hermes-agent's on_session_finalize event now maps to the new session-finalize subcommand → .sessionFinalize action → shared performAgentSessionTeardown(), which performs store.consume, clearAgentSurfaceResumeBinding, and clear_agent_pid. Previously it shared the same session-end subcommand as on_session_end, so sessionEndIsTurnBoundary: true would have suppressed the teardown for both events.
  • Integration test drives the full hook sequence (start → response → session-end × N → finalize) against a mock socket and asserts preservation of the restore record through per-turn events and its removal after finalize.

Confidence Score: 5/5

Safe to merge. The fix is minimal and correct: per-turn session-end is non-destructive, and true teardown only fires on session-finalize.

The key concern from the prior review iteration — both on_session_end and on_session_finalize routing to the same session-end subcommand — is fully addressed. on_session_finalize now routes to the new session-finalize subcommand and .sessionFinalize action, which calls performAgentSessionTeardown() unconditionally. The turn-boundary flag only guards the .sessionEnd case. grok and antigravity behavior is structurally unchanged. The integration test covers both the non-destructive path and the destructive teardown path.

No files require special attention.

Important Files Changed

Filename Overview
CLI/CMUXCLI+AgentHookDefinitions.swift Adds sessionEndIsTurnBoundary flag to AgentHookDef, sets it true for grok/antigravity/hermes-agent, adds sessionFinalize action, and re-routes hermes-agent's on_session_finalize event from session-end to the new session-finalize subcommand.
CLI/cmux.swift Extracts performAgentSessionTeardown() as a shared nested function, gates .sessionEnd destructive path on !def.sessionEndIsTurnBoundary, and adds the new .sessionFinalize switch case that calls performAgentSessionTeardown().
cmuxTests/CLIGenericHookPersistenceTests.swift Adds testHermesAgentSessionEndIsTurnBoundaryButFinalizeTearsDown — an integration test driving the full hook lifecycle and asserting both non-destructive turn-boundary behavior and destructive finalize cleanup.

Reviews (4): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ca4a712. Configure here.

Comment thread CLI/CMUXCLI+AgentHookDefinitions.swift
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/CLIGenericHookPersistenceTests.swift`:
- Around line 682-805: The new test
testHermesAgentSessionEndIsTurnBoundaryAndPreservesRestoreRecord in
CLIGenericHookPersistenceTests.swift uses XCTest but project guidelines require
Swift Testing; to avoid a large migration, leave this test as-is for consistency
with the existing XCTest-based suite, and create a tracked follow-up (create an
issue/PR) to migrate CLINotifyProcessIntegrationRegressionTests and related
XCTest suites to Swift Testing; add a brief TODO comment above
testHermesAgentSessionEndIsTurnBoundaryAndPreservesRestoreRecord referencing
that issue so reviewers know the deviation is intentional and trackable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 366211cd-6d8b-4399-b2e3-b2f7c395d304

📥 Commits

Reviewing files that changed from the base of the PR and between 239600b and ca4a712.

📒 Files selected for processing (3)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/cmux.swift
  • cmuxTests/CLIGenericHookPersistenceTests.swift

Comment thread cmuxTests/CLIGenericHookPersistenceTests.swift Outdated
…n action

Cursor Bugbot correctly flagged that hermes maps both on_session_end
(per-turn) and on_session_finalize (true teardown) to the session-end
subcommand, so the sessionEndIsTurnBoundary flag made the genuine teardown
non-destructive too — leaking the restore record, PID routing, and resume
binding indefinitely.

Route on_session_finalize to a new session-finalize subcommand backed by a
new AgentHookAction.sessionFinalize that owns the destructive cleanup. The
per-turn session-end still takes the non-destructive turn-boundary path; a
non-turn-boundary session-end (gemini/copilot/etc.) falls through to the
shared teardown, so their behavior is unchanged. Only hermes routes to the
new action.

Extend the regression test to also assert session-finalize consumes the
record, clears the resume binding, and clears agent PID routing.

Refs #5000

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 28067-28070: Extract the shared destructive cleanup that currently
uses `fallthrough` into a helper function (e.g.,
`performSessionFinalizationCleanup`) and call it from both the non-turn-boundary
teardown case and the `.sessionFinalize` case instead of falling through; the
helper should accept the `sessionId`, `store: AgentHookSessionStore`, and `env:
[String: String]`, perform the `store.lookup(sessionId:)` guard, call
`sendAgentFeedTelemetry(mapped.workspaceId)`, compute
`shouldSuppressNestedAgentVisibleMutations(currentAgentPID:mapped.pid, env:
env)`, and contain the rest of the destructive cleanup logic, then replace the
`fallthrough` with an explicit call to that helper from the original case and
keep `.sessionFinalize` calling the same helper so the logic is explicit and
SwiftLint warnings are removed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d030ab05-b9d0-41c8-bede-d3c10ff01ca7

📥 Commits

Reviewing files that changed from the base of the PR and between ca4a712 and 1abf05f.

📒 Files selected for processing (3)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/cmux.swift
  • cmuxTests/CLIGenericHookPersistenceTests.swift

Comment thread CLI/cmux.swift Outdated
Addresses CodeRabbit review: replace the fallthrough from .sessionEnd into
.sessionFinalize with an explicit, documented performAgentSessionTeardown()
nested helper called from both the non-turn-boundary session-end path and
the dedicated .sessionFinalize action. Behavior is unchanged; this removes
the SwiftLint 'fallthrough should be avoided' warning and makes the shared
destructive cleanup explicit.

Refs #5000

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@austinywang austinywang merged commit 01622de into main May 31, 2026
19 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hermes (hermes-agent) sessions never auto-restore — per-turn on_session_end deletes the restore record

1 participant