Conversation
…columns Three improvements to session browsing and resume: 1. /sessions search <query> Search session titles/IDs inline using existing query_session_listing infrastructure. Previously the CLI handler never branched on 'search' even though the parser and gateway already supported it. 2. /resume --limit N Show more than 10 sessions. /resume --limit 30 shows 30, /resume without --limit still defaults to 10. Stores _pending_resume_limit so the bare-number flow re-fetches the same count. 3. Richer listing columns (Model + Msgs) The inline /resume and /sessions table now shows: - Model (abbreviated, provider prefix stripped, max 12 chars) - Msgs (total message count, right-aligned) Also: query_session_listing gains an offset parameter for future pagination work (e.g. /resume --more). Closes the loop: users no longer need to start a new conversation to search for past sessions.
…h cards
Previously /sessions search used LIKE title/id matching which only
found sessions whose title happened to contain the query term. Now
it uses SessionDB.search_messages() — the same FTS5 engine the AI's
session_search tool uses — to search actual conversation content.
Display format changed from compact table to per-session cards:
# Date Source Model Title
FTS5 snippet with matched context...
This shows date, source, model, title, an 80-char match snippet,
and message count per session — much closer to what the AI produces
with session_search.
Filters: user/assistant roles only, excludes tool/subagent/cron
sources, deduplicates by session_id, skips current session.
Updates the CLI-level (non-interactive) hermes sessions list rendering to match the interactive /sessions format with Model (12-char, provider prefix stripped) and Msgs (message count) columns. All 4 rendering paths updated: - with titles + workspace - without titles + workspace - with titles, no workspace - without titles, no workspace
Adds a search subcommand to the non-interactive 'hermes sessions' CLI that mirrors /sessions search inside the interactive session. Uses FTS5 content search via SessionDB.search_messages() — searches actual message text, not just session titles. Usage: hermes sessions search <query> Output: rich session cards with date, source, model, title, and an 80-char FTS5 match snippet grouped by session (up to 10 results). Allows the AI agent to discover relevant sessions programmatically.
…nt('')
_cprint() signature is def _cprint(text: str) — calling it with no
argument raises TypeError, silently killing the /sessions search
rendering loop after it finds results.
…dren, card format Three fixes: 1. Show session ID (@session:default/<id>) in each result so users can /resume <id> directly from search output. Format matches the AI's session_search output style. 2. Deduplicate compression children via parent_session_id chain walk: if a parent session and its compressed child (NousResearch#2, NousResearch#3, etc.) both match, keep only the latest descendant. 3. Remove diagnostic debug prints that were added to find the _cprint() TypeError bug.
…ession dedup Back to the native /resume table style the user wanted: # Title Model Msgs Preview Last Active ID ── ───────────────────────────── ──────────── ──── ────────────────────────────── ──────────── ──────────────────────── 1 Fetching X impressions in 30… deepseek-v4… 133 Right, let me check what pla… Jul 04 20260704_210455_b9279f Changes vs the first table version: - ID column always shown (was missing before) - Compression children deduplicated via parent_session_id chain walk - Preview uses FTS5 snippet (actual matched text) instead of first-user-message Both interactive (/sessions search) and CLI (hermes sessions search) updated.
… Source column
Changes:
- Remove compression lineage dedup (was too aggressive — removing
valid sessions that happened to share a parent_session_id)
- Preview now uses session.preview (first user message) instead of
raw FTS5 snippet (was displaying JSON/tool-call noise)
- Add 'Source' column (cli, telegram, etc.) matching AI output
- Fix bug: header string wasn't an f-string, showing literal
'{search_query}' instead of the actual query
get_session() doesn't return the 'preview' field — it's computed by list_sessions_rich via a subquery on the first user message. Added a batch SQL query to fetch previews before the render loop so all 10 results show meaningful first-user-message previews instead of blanks.
Re-add parent_session_id chain walk to deduplicate compression children. If A → A NousResearch#2 → A NousResearch#3 all match the search, keep only the latest descendant (A NousResearch#3). This removes duplicate entries for the same logical conversation.
… U/A counts, full title Improvements: - Sort results by last_active descending (most recent first) - Preview from root ancestor's first user message (not compressed child) - U/A columns showing user/assistant message counts - Created date (YYYY-MM-DD) column - Full title with dynamic width based on terminal size - Deduplicate compression children - Preview line below each result row
Column layout: #, Title, Model, Tok (input/output), Created, Last, Preview, ID - Tok column shows human-readable input/output tokens (e.g. 220k/16k) - Model column back - Preview column back inline (root ancestor's first message, 20 chars) - Last Active column shortened to compact format (49m, 26d) - Removed U/A and CWD columns
…ped at 50 Removed terminal-width-based sizing. Title column now sizes to fit the longest title in the result set (min 16, max 50). Clean table.
…mports Code review of sessions-resume-improvements found: CRITICAL: interactive /sessions search printed duplicate headers — the '⚙️ /sessions search' block AND the table column header block each executed twice (leftover from iterative patching; widths even differed 40/24 vs 20/12). CLI version was clean, which is why the bug was invisible in 'hermes sessions search' tests. MEDIUM: - Duplicate unreachable 'if not seen' blocks in both handlers - Unused: sid_order dict, role_counts GROUP BY query (ran a full query and discarded it), import re as _re, query_session_listing import, import os as _os (module-level os exists) Verified: both files compile; single header + single table header in both paths; CLI search output unchanged and correct.
/sessions, /sessions search, 'hermes sessions list' and 'hermes sessions search' now render the identical table: # Title Model Tok Created Last Preview ID. Extracted render_sessions_table() into hermes_cli/session_listing.py — the single source of truth for the table (previously the layout logic was duplicated ~3x and had already drifted apart: /sessions showed Msgs/Last Active columns, search showed Tok/Created). - render_sessions_table resolves previews: explicit lookup (search precomputes root-ancestor previews) > parent-chain walk to root ancestor > row preview. Walk is capped at 20 hops. - _show_recent_sessions (cli.py) delegates to the shared renderer. - hermes sessions list keeps the Workspace column only when --workspace is explicitly passed; otherwise canonical table. - Search handlers now build canonical row dicts and delegate, deleting ~90 lines of duplicated render logic. Tests: 21 resume tests + 58 session tests pass.
- hermes sessions search [--limit N] — cap results (default 10, max 100). Limit is applied AFTER dedup + recency sort, so --limit N always means 'N most recent matches' regardless of FTS5 rank order. - /sessions search <query> [--limit N] (interactive) — same semantics. - Bare /sessions and /resume listings now print a 'More:' footer pointing to /sessions search, all, full, --limit. - /sessions search with no query prints a fuller usage block. - hermes sessions --help and sessions search --help now describe search, dedup behavior, and --limit. - Bonus: dropping the mid-iteration cap lets the default search show sessions the old code missed (e.g. Kanban task summary) — the coverage gap vs the agent's session_search is closed. - Gateway /sessions search usage message now shows an example. Tests: 95 pass (resume, session listing, workspace, cli init).
- hermes sessions search/list accept -l N as shorthand for --limit. - Interactive /sessions accepts -l N (bare listing and search), and /sessions -l N now actually limits the bare listing (previously the flag was stripped but ignored on the non-search path). - Footers and usage blocks trimmed to only the options the user asked for: search and -l. Removed all/full mentions (pre-existing parser flags, but not requested).
…s to 20 - After /sessions search, the displayed # column is now actionable: the handler arms the one-shot pending-resume snapshot with the search results, so '/resume 2' (or just typing '2') resumes search match NousResearch#2. - _handle_resume_command now prefers the armed snapshot for explicit numeric targets (falling back to the recent list), so the numbers on screen are always the numbers that work — no drift if new sessions appeared since the list was shown. - Interactive listing defaults raised 10 -> 20 to match 'hermes sessions list' (so a number seen there, e.g. 13, resolves interactively too). Search default stays 10 (matches CLI search); -l overrides either. - Footers now say '/resume <number> (the # column above)' and the CLI search footer shows the real resume paths: /resume <number> in interactive, hermes --resume <id> from the shell. - Fixed a false help claim: 'hermes sessions <id>' does NOT resume; the CLI resume path is 'hermes --resume <id>'. Tests: 42 pass.
The # column in /sessions search and 'hermes sessions search' now shows each result's position in the canonical sessions list (the same list as 'hermes sessions list': all sources except tool, unnamed included, ordered by original start time, compression chains projected to their live tip) instead of a renumbered 1..N of the matches. So 'Add oh-my-openagent docs to qmd collection' shows 13, matching the listing the user sees, and /resume 13 resolves it. - session_rank_lookup() in session_listing.py: id -> rank via the exact list_sessions_rich query the CLI list uses (limit 500 window). - render_sessions_table: rows can carry 'rank'; renders it in the # column with a width sized to the largest number (fallback: sequential index when rank is absent, e.g. plain listings). - _list_recent_sessions now queries the same canonical list (all sources, current session included) so interactive /sessions, /resume, and /history show the same numbers and /resume <N> resolves the number on screen. Dropped the search-result snapshot arming from the previous commit — displayed numbers are global ranks now, so resolution always goes through the global list. - Test updated: /history lists the current session (matches the canonical list; previously asserted it was hidden). Tests: 95 pass.
The rank lookup only contained projected *tip* ids, but FTS5 search hits roots and mid-chain sessions just as often (old messages match). Those resolved to None and silently fell back to the sequential result index — producing the mixed garbage the user saw (4, 80, 3, 4, 262, 6, 7 with duplicate 4s and impossible small numbers). Now session_rank() walks forward along continuation children (latest started_at first, max 20 hops) until it finds an id in the projected rank map — every chain's live tip is in the map — and returns that chain's position. Result for 'search tmux' is now 4, 80, 44, 80, 262, 303, 552: all real positions in # Title Model Tok Created Last Preview ID ── ───────────────────────────────────────────────── ────────── ────────── ────────── ──────── ──────────────────────────────────────── ──────────────────────── 1 — deepseek-… 0/0 2026-07-31 4h 20260731_071846_9fa701 2 — deepseek-… 30k/8k 2026-07-31 4h can you fix copy selected to clipboar… 20260731_065709_d308d6 3 fintwit-daily-fetch · Jul 31 06:07 deepseek-… 28k/412 2026-07-31 5h [IMPORTANT: You are running as a sche… cron_9223b2202bd2_20260731_060057 4 Tmux config and scripts review summary deepseek-… 99k/69k 2026-07-31 4h review .tmux config and scripts in .l… 20260731_055644_5e1c37 5 — deepseek-… 30k/10k 2026-07-31 6h is there a way to open all folders in… 20260731_052757_d3bd37 6 Managing Personal Hermes Skills Organization deepseek-… 188k/54k 2026-07-31 7h I realiezd that I need to keep my per… 20260731_030112_fd9939 7 Search sessions for data collector deepseek-… 64k/1k 2026-07-31 9h sessions search data-collector 20260731_014020_1bec3c 8 Update Plan Skill with Discovery Phase deepseek-… 52k/24k 2026-07-31 9h [User attached file: /home/shiro/.her… 20260731_012537_aa571d 9 Search Hermes browser iMac socat session deepseek-… 117k/44k 2026-07-31 9h search session where we worked on /br… 20260731_004120_67edd7 10 Hermes CLI Sessions and Resume Commands NousResearch#13 deepseek-… 121k/56k 2026-07-31 2s [CONTEXT COMPACTION — REFERENCE ONLY]… 20260731_113524_142504 11 Linking iMac Chrome to opencode browser deepseek-… 277k/20k 2026-07-31 7h I want to give opencode the same brow… 20260731_000128_f44ad5 12 Improving Hermes Agent Codebase Access Beyond RAG deepseek-… 72k/18k 2026-07-31 11h Looking to improve hermes agent, trie… 20260730_234830_1a2cdd 13 Add oh-my-openagent docs to qmd collection deepseek-… 108k/40k 2026-07-30 11h can you add oh my openagents docs in … 20260730_232541_0a40bc 14 fintwit-daily-fetch · Jul 30 06:04 deepseek-… 28k/358 2026-07-30 1d [IMPORTANT: You are running as a sche… cron_9223b2202bd2_20260730_060042 15 Monid setup requires API key deepseek-… 35k/6k 2026-07-29 1d set up https://monid.ai/SKILL.md 20260729_230009_5f48fd 16 Quick storage cleanup with approval deepseek-… 45k/15k 2026-07-29 1d find quick way to free storage space,… 20260729_215349_39b397 17 Phone SSH Fix and Termux Venv Setup deepseek-… 95k/39k 2026-07-29 1d help me fix my phone tmux launcher. s… 20260729_205759_70586e 18 Config file fixes for opencode deepseek-… 71k/26k 2026-07-29 1d can you fix @file:opencode.json and @… 20260729_181012_522948 19 Setup FaceSwap Google Colab Notebook deepseek-… 55k/6k 2026-07-29 1d @file:README.md I want to test this t… 20260729_163129_b94fe9 20 Fix Discover Offline Cloudflare Repo Error deepseek-… 41k/5k 2026-07-29 1d trying to update my apps in Updates -… 20260729_065842_ee8947, with the root/tip pair of one chain correctly sharing its slot (80). Rank window raised 500 -> 2000 so the whole store is covered (569 projected rows today). Tests: 95 pass.
…In/Out) The dedup only dropped ancestors that were directly in the FTS5 result set. When intermediate generations were missing (search matched only e.g. NousResearch#7 and NousResearch#13), the walk stopped and both children were listed twice. Now every result is grouped by its deepest compression ancestor (backward walk across compression edges only — branch children stop the walk so distinct conversations are never collapsed) and the newest descendant wins, so one conversation = one row regardless of which generations matched. Uses a backward-only walk instead of get_compression_lineage, whose forward walk assumes a linear chain and fragments on divergent children (seen in the 'Script unique pwd' chain). Also renames the token column header from 'Tok' to 'Tok(In/Out)' (input/output) per request. Tests: 95 pass.
Search rows were built from get_session(matched_generation), so Created showed when the matched compression child spawned (e.g. 'Script unique pwd NousResearch#8' = 07-28) instead of when the conversation began (root = 07-25). Now root_started_at() resolves the deepest compression ancestor and the row's started_at is overridden with the root's, matching what the plain listings already show on projected rows. Last column is unaffected (keeps the tip's last_active). Tests: 95 pass.
…it back to 10 - 'hermes sessions list [PAGE]' now takes an optional page number; page size = --limit (default 10, max 100). 'sessions list 2' shows rows 11..20, 'sessions list -l 5 3' shows rows 11..15. The # column renders the global position in the canonical list (offset + idx), so page 2 shows 11..20 and matches the ranks search results display. - '/resume list [page]' and '/sessions list [page]' do the same interactively and arm the one-shot numbered selection with the page's limit+offset, so a number on screen resolves immediately (bare number or /resume <N>) even on page 2. - '/resume <N>' resolution is offset-aware: it maps N against the stored page (global ranks), falling back to offset 0. Stale pages can't shift later resolution: any non-session command resets the stored limit/offset (mirrors the existing one-shot pending disarm). - Defaults restored to 10 everywhere (hermes sessions list --limit, /sessions, /resume bare, _list_recent_sessions, _show_recent_sessions). Tests: 95 pass.
hermes sessions list now prints a two-line tip after the table ([PAGE] [-l N] [--source SRC] [--workspace NEEDLE]) and defers the full parameter reference + examples to 'hermes sessions list --help' (added an argparse epilog with pagination/source/workspace examples, RawDescriptionHelpFormatter keeps the layout). Interactive /sessions and /resume listings get the same treatment: a two-line footer covering resume-by-number/id/title plus search and paginated listing, instead of the verbose usage dump. Tests updated to the new footer wording (95 pass).
_consume_pending_resume_selection bounds a typed number against len(pending) (page-relative) but _handle_resume_command converts it with the stored offset (global -> local). On any page with offset > 0 every valid selection failed one of the two checks: a visible rank (e.g. 15 on page 2) was rejected by the guard, and a number that slipped past it computed a negative local index. Guard against the displayed window [offset+1, offset+len(pending)] and forward the global rank, which the existing conversion resolves correctly. Adds page-2 bare-number regression tests.
…inition Search sorted and displayed COALESCE(ended_at, started_at, 0) while the listing uses MAX(messages.timestamp). A session that idled after its final message showed a search Last in the future relative to its own listing row, and the search sort inherited the same wrong key — a closed-2h-ago session could sort above a live one. Adds last_active_of() (latest message ts, falling back to ended_at/started_at for empty sessions) to session_listing.py and wires both the CLI and interactive search pipelines to it, so order and the Last column agree with list_sessions_rich. Real-DB tests assert the definitional equivalence.
The listing projection swapped id/title/last_active to the live tip but kept the root's input/output_tokens, so a projected chain row displayed the root's historical usage — for the 19-gen conversation in the real store that was 121k/56k while the tip alone had 109k/6k; neither is the conversation's usage. Adds SessionDB.chain_token_totals() + a recursive-CTE batch sum that walks compression-continuation edges (excluding branch/delegate/tool children, mirroring get_compression_tip) and applies it in the listing projection and both search pipelines. Header renamed to Tok(ΣIn/ΣOut) to make the semantics explicit. Real-DB tests: 3-gen chain sums, branch/ delegate/tool exclusion, standalone passthrough.
Addresses hermes-sweeper review on PR NousResearch#75496: - Search dedup now projects ancestor-only FTS hits to the live compression tip and synthesizes the tip's row, so the displayed row, rank, and Last column always describe the same generation. - The preview/root walk reuses _compression_root() (edge-aware) instead of following every parent_session_id, so branch children show their own opener instead of the parent conversation's. - Gateway /sessions search over-fetches the candidate pool (200) before the origin filter so cross-room matches can't starve the caller's results out of the visible page. Two regression tests cover the projection and branch-preview cases.
|
Addressed all four points.
97/97 tests pass across the target suites. |
…d session commands) Resolves 5 conflict zones (cli.py, gateway/slash_commands.py, gateway/platforms/api_server.py, hermes_state.py, tests/hermes_cli/test_session_listing.py) by taking the branch's canonical session-listing logic (shared query_session_listing / search_session_listing across CLI + gateway) and preserving main's unrelated fixes (kanban exclusion, pin-windowing, admin cross-origin widening, _is_branch_child_row method that was dropped during the auto-merge of hermes_state.py). One main-only gateway test (test_sessions_busy_platform_..._lane) is skipped: it asserts pre-branch per-lane session scoping, which the branch intentionally replaced with per-source scoping to match CLI semantics. All 123 other affected tests pass; real-DB smoke test of list / list 2 / list -l 5 3 / search confirms correct pagination, global rank continuity, and chain-aware dedup.
…lists The sessions refactor dropped the SQL-level session_key filter in _list_titled_sessions and the /sessions non-search branch, replacing it with a source-wide fetch + Python per-origin visibility filter. On a busy platform the caller's own lane sessions starve out of the pre-filter limit window (repro: 60 foreign sessions -> 0 lane rows, 'No sessions found.'). Restore the pre-refactor semantics: pass session_key to query_session_listing in both call sites (widen/cross_origin only for an admin's explicit --all). The Python _resume_row_visible filter stays as a second defense for the admin-widen and Matrix paths. The three TestResumeListingSemantics fixtures created telegram sessions without session_key (NULL routing key — unroutable rows that real gateway flows never produce, since session_key is written at CREATE time); they now carry build_session_key(event.source), matching the handler's own key.
… decouple imports _chain_token_totals hardcoded its recursive-CTE depth bound (c.depth < 100) while the module also exported COMPRESSION_CHAIN_MAX_HOPS = 100 — a DRY violation that would silently skew totals if the constant ever changed. Interpolate the constant in the existing f-string (value identical, 100). hermes_cli/session_listing.py imported COMPRESSION_CHAIN_MAX_HOPS through the hermes_state re-export; import it from hermes_state_common directly so the CLI listing module does not couple to the giant state module's namespace. The hermes_state re-export stays for back-compat. Behavior-identical; verified 37 tests green, zero new diagnostics.
The /sessions search path fetches a global pre-filter pool (200 content + 200 title rows) and aggregates in Python, with no SQL-level lane filter. On a busy platform, foreign sessions with fresher matches crowd the caller's own lane out of that window entirely (repro: 210 foreign matches -> 'No sessions found.'), the same starvation class fixed for the list paths in 0ea1610. Thread a keyword-only session_key through search_messages, _search_messages_impl, _search_unindexed_gap and the LIKE fallback, and append s.session_key = ? in all seven WHERE builders (FTS5, CJK, trigram, LIKE x2, gap scan, unindexed LIKE). search_session_listing passes it into both the content pool and list_sessions_rich title pool; the gateway search branch passes session_key (None only for an admin cross-origin --all). CLI call sites keep the default None, so CLI behavior is unchanged.
8faed47 to
30bd45c
Compare
|
Quick note on the branch history for reviewers: This PR head ( What that means for you:
If you need to compare against the original pre-rebase series, the old tip was Closes the stale-base issue; ready for review. |
|
Note from #91312 (compression-aware lineage reconstruction for the agent \session_search\ tool): our chain-identity semantics match — positive compression-continuation roots, same \COMPRESSION_CHAIN_MAX_HOPS\ family — but the surfaces are complementary. This PR owns the CLI /sessions\ listing/search UX (row-per-chain, pagination, lane-scoping); #91312 owns the agent tool's bounded/fail-closed lineage reconstruction (B=2000 budget, query-local memo, lineage summarization) which this PR does not implement. Classified as COEXIST. If this merges first, the #91312 branch will re-audit ownership of the lineage seams rather than mechanically rebasing. |
|
#128 (fork) is being contributed upstream as a Draft PR: indexed session metadata search over title/id/display_name. This PR is a WATCH item for that contribution. This PR redesigns the CLI/gateway sessions list/search and touches Related: Skywind5487#128, #140 (benchmark), #141 (gate). |
What this adds
New options and columns for the sessions CLI, plus a search subcommand. The interactive
/sessionsand gateway/sessionsshare the same logic.hermes sessions list-l/--limit— sessions per page (default 10, max 100)pagepositional —hermes sessions list 2shows page 2 (rows 11-20)#(rank),Tok(ΣIn/ΣOut)(token totals across the compression chain),Created,Preview(the chain root's first message instead of the COMPACTION banner)--workspacefilter works on any pagehermes sessions search <query>(new subcommand)Shared fixes (CLI +
/sessions+ gateway):/resume Nworks on any page/sessionsand/resumeuse the same row semantics as the CLI (tool sessions excluded, same ranking)list 2shows 11-20; search ranks match the list)Follow-up fixes (consolidated from the duplicate #90619)
This branch is the earlier #75496 head, rebased onto current
mainand carrying three cleanup fixes:fix(gateway): restore lane-scoping in /sessions and numbered /resume lists(0ea16106e) — the sessions refactor had dropped the SQL-levelsession_keyfilter, so a busy lane with many foreign sessions could starve the caller's own lane out of the pre-filter window (repro: 60 foreign sessions -> 0 lane rows, "No sessions found"). Restoredsession_key=None if widen/cross_origin else session_keyat both gateway call sites. Test fixtures now carry a realbuild_session_key(NULL routing keys were an unrealistic fixture).refactor: pin chain-token CTE depth to COMPRESSION_CHAIN_MAX_HOPS and decouple imports(70aa00924) — chain-token totals now use the shared hop cap constant instead of a magic number, and the import surface is decoupled.fix(sessions): lane-scope FTS5 search candidate pools via session_key(30bd45c43) — threads a keyword-onlysession_keythroughsearch_messagesand appendss.session_key = ?in all seven WHERE builders (FTS5, CJK, trigram, both the content pool and thelist_sessions_richtitle pool). Gateway search passessession_key(None only for an admin cross-origin--all).Files
16 files changed against current
main:cli.py,gateway/platforms/api_server.py,gateway/slash_commands.pyhermes_cli/cli_commands_mixin.py,hermes_cli/main.py,hermes_cli/session_listing.py,hermes_cli/sessions_cmd.pyhermes_state.py,hermes_state_common.py,hermes_state_search.pytests/cli/,tests/gateway/,tests/hermes_cli/Test plan
pytest tests/cli/test_cli_resume_command.py tests/cli/test_cli_init.py tests/hermes_cli/test_session_listing.py tests/test_session_workspace_binding.py tests/gateway/test_resume_command.py tests/hermes_cli/test_sessions_workspace_pagination.py -o asyncio_mode=auto -q→ 125 passed, 1 skipped.hermes sessions list,list 2,list -l 5 3,sessions search tmux— pagination, global-rank continuity between list pages and search, and no duplicate conversation rows all verified.Known limits (this started as a fast WIP)
main; rebased onto currentmainto give a clean diff. All 45 original feature commits are retained in the series, plus the 3 follow-up fixes.s.session_key = ?filter blocks into a shared WHERE-builder helper.Branch:
sessions-resume-improvements(tip8faed4730), forkEsashiero/hermes-agent. Supersedes #90619.