fix(sessions): one-shot runs get a distinct oneshot source that pickers hide

`hermes chat -q`/`--oneshot`/`-Q` and `hermes -z` (both set HERMES_SINGLE_QUERY_SESSION=1)
persisted their session as `cli` — and, before the first pass, as the inherited
`tui`/`desktop` transport label — so finite automation runs sat in the TUI, Desktop and
dashboard session pickers next to real conversations (#112550).

- run_agent._session_source_for_agent: a single-query run whose source is empty (or an
  inherited UI transport label without an explicit --source) resolves to `oneshot`; the
  platform gate keeps delegate children (`subagent`) untouched; an explicit `--source`
  (HERMES_SESSION_SOURCE_EXPLICIT=1 from main.py) still wins.
- hermes_state_sessions.INTERNAL_LISTING_SOURCES = (kanban, tool, oneshot) replaces the
  three copied `["kanban", "tool"]` literals (tui_gateway session.list, console
  `sessions list`/`stats`, in-chat /sessions), and the Desktop project tree / sidebar
  recents and the dashboard automation set exclude `oneshot` too.
- `hermes -c` / `--resume latest` still chain on the previous one-shot (PR #105957's
  documented flow): the CLI MRU lookup matches the cli family {cli, oneshot} and
  search_sessions accepts several sources; one-shots keep stamping their launch cwd so the
  workspace-scoped lookup keeps working.
- Compression child: the rotated child is published with the PARENT ROW's persisted source
  instead of bare agent.platform, so a `--source tool` / `oneshot` / inherited `kanban`
  session does not degrade to a picker-visible `cli` row after compaction.
- Docs: sessions source table (+ oneshot/kanban/tool rows, compression note) and the
  `--source` flag reference (explicit flag always stored as given).
This commit is contained in:
teknium1
2026-09-16 22:19:03 -07:00
committed by Teknium
parent 9790d730c0
commit ae1b5d79b2
17 changed files with 165 additions and 25 deletions

View File

@@ -8,7 +8,7 @@ import re
import sqlite3
import time
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
from agent.session_activity import (
ActivityProvenance, bound_activity_description, normalize_activity_provenance,
@@ -170,6 +170,11 @@ def _delete_delegate_children(conn, parent_ids: List[str]) -> List[str]:
# Lifecycle statuses surfaced by session pickers; classified from the final
# message row ONLY so it stays O(1) per session.
# Sessions that are not human conversations (kanban workers, third-party tool integrations, finite one-shot
# runs): every human picker — TUI/Desktop session lists, ``/sessions`` in the CLI, ``sessions list`` in the
# console — excludes them. A deny-list, so new interactive platforms surface automatically.
INTERNAL_LISTING_SOURCES = ("kanban", "tool", "oneshot")
SESSION_STATUS_COMPLETE = "complete"
SESSION_STATUS_INTERRUPTED = "interrupted"
SESSION_STATUS_ERROR = "error"
@@ -1378,15 +1383,17 @@ class SessionSessionsMixin:
return list(reversed(chain)) or [session_id]
def search_sessions(
self, source: str = None, limit: int = 20, offset: int = 0, workspace_key: str = None,
self, source: Union[str, Sequence[str], None] = None, limit: int = 20, offset: int = 0,
workspace_key: str = None,
) -> List[Dict[str, Any]]:
"""Sessions MRU-first with a computed ``last_active``; ``workspace_key`` scopes to one workspace
so ``hermes -c``/``--resume`` picks its last session."""
so ``hermes -c``/``--resume`` picks its last session. ``source`` may be one label or several."""
where_clauses = []
params: list = []
if source:
where_clauses.append("s.source = ?")
params.append(source)
sources = [source] if isinstance(source, str) else list(source)
where_clauses.append(f"s.source IN ({','.join('?' * len(sources))})")
params.extend(sources)
if workspace_key:
ws_clause, ws_params = _workspace_key_clause(workspace_key)
where_clauses.append(ws_clause)