diff --git a/hermes_cli/main.py b/hermes_cli/main.py
index 968745704b..40de1f125e 100644
--- a/hermes_cli/main.py
+++ b/hermes_cli/main.py
@@ -596,15 +596,32 @@ def _session_browse_picker(sessions: list) -> Optional[str]:
def _resolve_last_session(source: str = "cli") -> Optional[str]:
- """Look up the most recent session ID for a source."""
+ """Look up the most recently *used* session ID for a source.
+
+ Previously this returned the most recently *started* session, which meant
+ `hermes -c` could skip the session you just closed if a newer one had been
+ opened earlier in a different window. We now order by last_active
+ (max message timestamp, falling back to started_at) so -c always resumes
+ the most recent conversation you actually touched.
+ """
try:
from hermes_state import SessionDB
db = SessionDB()
- sessions = db.search_sessions(source=source, limit=1)
+ sessions = db.search_sessions(source=source, limit=20)
db.close()
- if sessions:
- return sessions[0]["id"]
+ if not sessions:
+ return None
+
+ def _last_active(s: dict) -> float:
+ v = s.get("last_active") or s.get("started_at") or 0
+ try:
+ return float(v)
+ except (TypeError, ValueError):
+ return 0.0
+
+ sessions.sort(key=_last_active, reverse=True)
+ return sessions[0]["id"]
except Exception:
pass
return None
diff --git a/tests/hermes_cli/test_resolve_last_session.py b/tests/hermes_cli/test_resolve_last_session.py
new file mode 100644
index 0000000000..68abc3df50
--- /dev/null
+++ b/tests/hermes_cli/test_resolve_last_session.py
@@ -0,0 +1,61 @@
+"""Verify `hermes -c` picks the session the user most recently used."""
+
+from __future__ import annotations
+
+from hermes_cli.main import _resolve_last_session
+
+
+class _FakeDB:
+ def __init__(self, rows):
+ self._rows = rows
+ self.closed = False
+
+ def search_sessions(self, source=None, limit=20, **_kw):
+ rows = [r for r in self._rows if r.get("source") == source] if source else list(self._rows)
+ return rows[:limit]
+
+ def close(self):
+ self.closed = True
+
+
+def test_resolve_last_session_prefers_last_active_over_started_at(monkeypatch):
+ # `search_sessions` returns in started_at DESC order, but the most recently
+ # *touched* session may have been started earlier. -c should pick by
+ # last_active so closing the active session and typing `hermes -c` resumes
+ # that one, not an older-but-newer-started session from another window.
+ rows = [
+ {
+ "id": "new_started_old_active",
+ "source": "cli",
+ "started_at": 1000.0,
+ "last_active": 100.0,
+ },
+ {
+ "id": "old_started_recently_active",
+ "source": "cli",
+ "started_at": 500.0,
+ "last_active": 999.0,
+ },
+ ]
+
+ fake_db = _FakeDB(rows)
+ monkeypatch.setattr("hermes_state.SessionDB", lambda: fake_db)
+
+ assert _resolve_last_session("cli") == "old_started_recently_active"
+ assert fake_db.closed
+
+
+def test_resolve_last_session_returns_none_when_empty(monkeypatch):
+ monkeypatch.setattr("hermes_state.SessionDB", lambda: _FakeDB([]))
+ assert _resolve_last_session("cli") is None
+
+
+def test_resolve_last_session_falls_back_to_started_at(monkeypatch):
+ # When last_active is missing entirely (legacy row), fall back to
+ # started_at so the helper still picks the newest session.
+ rows = [
+ {"id": "older", "source": "cli", "started_at": 10.0},
+ {"id": "newer", "source": "cli", "started_at": 20.0},
+ ]
+ monkeypatch.setattr("hermes_state.SessionDB", lambda: _FakeDB(rows))
+ assert _resolve_last_session("cli") == "newer"
diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx
index 9e716583c9..0c13640765 100644
--- a/ui-tui/src/components/appLayout.tsx
+++ b/ui-tui/src/components/appLayout.tsx
@@ -8,6 +8,7 @@ import { $isBlocked, $overlayState, patchOverlayState } from '../app/overlayStor
import { $uiState } from '../app/uiStore.js'
import { PLACEHOLDER } from '../content/placeholders.js'
import { inputVisualHeight, stableComposerColumns } from '../lib/inputMetrics.js'
+import { PerfPane } from '../lib/perfPane.js'
import { AgentsOverlay } from './agentsOverlay.js'
import { GoodVibesHeart, StatusRule, StickyPromptTracker, TranscriptScrollbar } from './appChrome.js'
@@ -248,23 +249,31 @@ export const AppLayout = memo(function AppLayout({
{overlay.agents ? (
-
+
+
+
) : (
-
+
+
+
)}
{!overlay.agents && (
<>
-
+
+
+
-
+
+
+
>
)}
diff --git a/ui-tui/src/lib/perfPane.tsx b/ui-tui/src/lib/perfPane.tsx
new file mode 100644
index 0000000000..32b260b721
--- /dev/null
+++ b/ui-tui/src/lib/perfPane.tsx
@@ -0,0 +1,82 @@
+// Perf instrumentation: wraps React.Profiler around named panes and writes
+// commit timings to a log file when HERMES_DEV_PERF is set. Enabled per-run
+// via the env var; zero-cost (Profiler is replaced by a Fragment) when off.
+//
+// Log format: one JSON object per line, for easy `jq` filtering. We only
+// log commits that exceed a threshold (default 2ms) so the file doesn't
+// fill up with sub-millisecond idle renders. Tune via HERMES_DEV_PERF_MS.
+//
+// Usage in consumers:
+// import { PerfPane } from './perfPane.js'
+// ...
+//
+// Inspect with:
+// tail -f ~/.hermes/perf.log | jq -c 'select(.actualMs > 8)'
+// jq -s 'group_by(.id) | map({id: .[0].id, count: length, p50: (sort_by(.actualMs) | .[length/2|floor].actualMs), p99: (sort_by(.actualMs) | .[length*0.99|floor].actualMs)})' ~/.hermes/perf.log
+
+import { appendFileSync, mkdirSync } from 'node:fs'
+import { homedir } from 'node:os'
+import { dirname, join } from 'node:path'
+
+import { Profiler, type ProfilerOnRenderCallback, type ReactNode } from 'react'
+
+const ENABLED = /^(?:1|true|yes|on)$/i.test((process.env.HERMES_DEV_PERF ?? '').trim())
+const THRESHOLD_MS = Number(process.env.HERMES_DEV_PERF_MS ?? '2') || 2
+const LOG_PATH = process.env.HERMES_DEV_PERF_LOG?.trim() || join(homedir(), '.hermes', 'perf.log')
+
+let initialized = false
+
+const ensureLogDir = () => {
+ if (initialized) {
+ return
+ }
+
+ initialized = true
+
+ try {
+ mkdirSync(dirname(LOG_PATH), { recursive: true })
+ } catch {
+ // Best-effort — if we can't create the dir (readonly fs, /tmp, etc.)
+ // the appendFileSync calls below will throw silently and we drop the
+ // sample. Perf logging should never crash the TUI.
+ }
+}
+
+const onRender: ProfilerOnRenderCallback = (id, phase, actualMs, baseMs, startTime, commitTime) => {
+ if (actualMs < THRESHOLD_MS) {
+ return
+ }
+
+ ensureLogDir()
+
+ const row = {
+ actualMs: Math.round(actualMs * 100) / 100,
+ baseMs: Math.round(baseMs * 100) / 100,
+ commitMs: Math.round(commitTime * 100) / 100,
+ id,
+ phase,
+ startMs: Math.round(startTime * 100) / 100,
+ ts: Date.now()
+ }
+
+ try {
+ appendFileSync(LOG_PATH, `${JSON.stringify(row)}\n`)
+ } catch {
+ // Same rationale as ensureLogDir — never crash the UI to log a sample.
+ }
+}
+
+export function PerfPane({ children, id }: { children: ReactNode; id: string }) {
+ if (!ENABLED) {
+ return children
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const PERF_ENABLED = ENABLED
+export const PERF_LOG_PATH = LOG_PATH