Skip to content

feat(cli): sessions list/search options, columns, pagination - #75496

Open
Esashiero wants to merge 49 commits into
NousResearch:mainfrom
Esashiero:sessions-resume-improvements
Open

Esashiero wants to merge 49 commits into
NousResearch:mainfrom
Esashiero:sessions-resume-improvements

Conversation

@Esashiero

@Esashiero Esashiero commented Jul 31, 2026

Copy link
Copy Markdown

What this adds

New options and columns for the sessions CLI, plus a search subcommand. The interactive /sessions and gateway /sessions share the same logic.

hermes sessions list

  • -l/--limit — sessions per page (default 10, max 100)
  • page positional — hermes sessions list 2 shows page 2 (rows 11-20)
  • new columns: # (rank), Tok(ΣIn/ΣOut) (token totals across the compression chain), Created, Preview (the chain root's first message instead of the COMPACTION banner)
  • --workspace filter works on any page

hermes sessions search <query> (new subcommand)

  • full-text search over session messages (FTS5), results in the same table format

Shared fixes (CLI + /sessions + gateway):

  • bare-number /resume N works on any page
  • one row per compression chain (latest descendant wins); ranks and previews follow the chain root
  • gateway /sessions and /resume use the same row semantics as the CLI (tool sessions excluded, same ranking)
  • pagination continues global numbering across list pages and search (list 2 shows 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 main and carrying three cleanup fixes:

  1. fix(gateway): restore lane-scoping in /sessions and numbered /resume lists (0ea16106e) — the sessions refactor had dropped the SQL-level session_key filter, 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"). Restored session_key=None if widen/cross_origin else session_key at both gateway call sites. Test fixtures now carry a real build_session_key (NULL routing keys were an unrealistic fixture).
  2. 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.
  3. fix(sessions): lane-scope FTS5 search candidate pools via session_key (30bd45c43) — threads a keyword-only session_key through search_messages and appends s.session_key = ? in all seven WHERE builders (FTS5, CJK, trigram, both the content pool and the list_sessions_rich title pool). Gateway search passes session_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.py
  • hermes_cli/cli_commands_mixin.py, hermes_cli/main.py, hermes_cli/session_listing.py, hermes_cli/sessions_cmd.py
  • hermes_state.py, hermes_state_common.py, hermes_state_search.py
  • 5 test files under tests/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 -q125 passed, 1 skipped.
  • Manual smoke test against the real session DB: 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)

  • The branch was originally a 3-week-old fork of main; rebased onto current main to give a clean diff. All 45 original feature commits are retained in the series, plus the 3 follow-up fixes.
  • No docs update, no changelog entry yet.
  • A future cleanup could collapse the now-repeated s.session_key = ? filter blocks into a shared WHERE-builder helper.

Branch: sessions-resume-improvements (tip 8faed4730), fork Esashiero/hermes-agent. Supersedes #90619.

Esashiero added 30 commits July 31, 2026 01:28
…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.
@Esashiero

Copy link
Copy Markdown
Author

Addressed all four points.

  1. Ancestor-only hits now project to the tip. search_session_listing resolves each kept hit via get_compression_tip(); when the hit is an old generation, the tip's row is synthesized from its session meta, so the displayed row, # rank, and Last column describe the same generation. Covered by test_ancestor_only_hit_projects_to_tip.
  2. Preview/root walk is edge-aware. The search preview walk now reuses _compression_root() (branch/delegate/tool edges stop the walk) instead of following every parent_session_id — a branch child previews its own opener, not the parent conversation's. Covered by test_branch_child_preview_does_not_cross_into_parent.
  3. Gateway search over-fetches the candidate pool (limit 200) before the origin filter, so invisible-origin matches can't starve the caller's rows out of the visible page.
  4. Diff cleanup done. .backups/ review copies, .beads/ hooks/config, and the bd/dolt edits to AGENTS.md and .gitignore were removed from the branch in 6de17ee3a — the diff is now 16 files (feature + tests only).

97/97 tests pass across the target suites.

@Esashiero
Esashiero marked this pull request as ready for review July 31, 2026 16:26
@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists labels Jul 31, 2026
…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.
@Esashiero
Esashiero force-pushed the sessions-resume-improvements branch 2 times, most recently from 8faed47 to 30bd45c Compare August 20, 2026 12:18
@Esashiero

Copy link
Copy Markdown
Author

Quick note on the branch history for reviewers:

This PR head (sessions-resume-improvements) was originally opened 3 weeks ago from a fork point that is now ~4381 commits behind current main. To make the diff reviewable (14 files instead of 4412), I rebased the 45 original feature commits + 3 follow-up fixes onto today's main and force-updated the branch.

What that means for you:

If you need to compare against the original pre-rebase series, the old tip was 3ad71baca on the same branch name (accessible via GitHub's reflog if needed).

Closes the stale-base issue; ready for review.

@Skywind5487

Copy link
Copy Markdown
Contributor

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.

@Skywind5487

Copy link
Copy Markdown
Contributor

#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 hermes_state*.py; the #128 contribution adds indexed metadata discovery in list_sessions_rich. They are orthogonal today — the #128 branch rebases cleanly on current main (merge-tree zero-conflict). If this PR merges before #128 lands, #128 will re-run ownership before rebasing.

Related: Skywind5487#128, #140 (benchmark), #141 (gate).

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/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data 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.

4 participants