refactor(agent/E_session): compact docstrings/comments by hand, keep every WHY

This commit is contained in:
Teknium
2026-09-02 19:13:00 -07:00
parent b269b9c2a3
commit 59dfa83260
6 changed files with 95 additions and 179 deletions

View File

@@ -51,14 +51,9 @@ def _orphan_recovery(name: str, unknown_text: str, none_text: str) -> tuple:
def strip_interrupted_tool_tails(agent_history: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Strip interrupted assistant→tool sequences from replay history.
The interrupted block is not necessarily the final tail (a queued real user message may
follow it), so every contiguous assistant(tool_calls)+tool-result block containing an
interrupted result is handled; successful sequences stay intact. Read-only blocks are
dropped; blocks with a side-effecting call are KEPT with the interrupted results rewritten
as orphan-recovery notices, since the effect may already have happened.
"""
"""Strip interrupted assistant→tool blocks anywhere in replay history (a queued user message may
follow one). Read-only blocks are dropped; blocks with a side-effecting call are KEPT with the
interrupted results rewritten as orphan-recovery notices, since the effect may have happened."""
if not agent_history:
return agent_history
@@ -110,14 +105,10 @@ def strip_interrupted_tool_tails(agent_history: List[Dict[str, Any]]) -> List[Di
def strip_dangling_tool_call_tail(agent_history: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Strip a trailing ``assistant(tool_calls)`` block left with NO answers.
A tool call that kills the gateway process itself (``docker restart``, ``hermes gateway
restart``) is SIGKILLed mid-call, before any tool result or the orderly shutdown rewind; the
persisted tail has zero matching ``tool`` rows, which ``strip_interrupted_tool_tails`` cannot
detect. Only acts when the tail has NO tool answers — a partially answered block still
resumes. Read-only tails are dropped; side-effecting ones get synthetic UNKNOWN-effect results.
"""
"""Strip a trailing ``assistant(tool_calls)`` with NO answers — a call that killed the gateway
itself (``docker restart``) left zero ``tool`` rows, which ``strip_interrupted_tool_tails`` cannot
detect. A partially answered block still resumes. Read-only tails are dropped; side-effecting
ones get synthetic UNKNOWN-effect results."""
if not agent_history:
return agent_history
@@ -197,14 +188,9 @@ def strip_stale_dangerous_confirmations(
now: float,
expiry_seconds: float = _DANGEROUS_CONFIRMATION_EXPIRY_SECONDS,
) -> List[Dict[str, Any]]:
"""Expire stale dangerous-confirmation text in user messages.
If a host restart killed the gateway before the tool result was written, the user's
confirmation phrase survives in the transcript; a casual "are you there?" minutes later can
read to the model as a fresh re-confirmation. Expired confirmations are REDACTED IN PLACE.
Messages without a timestamp (legacy transcripts, test scaffolding) and confirmations still
inside the expiry window are left untouched.
"""
"""Redact IN PLACE dangerous-confirmation text older than ``expiry_seconds`` in user messages: a
confirmation surviving a restart can read as a fresh re-confirmation minutes later. Messages
without a timestamp (legacy transcripts, test scaffolding) are left untouched."""
if not agent_history:
return agent_history

View File

@@ -59,11 +59,8 @@ def _is_ephemeral_scaffolding(msg: Any) -> bool:
def _safe_session_filename_component(session_id: str) -> str:
"""Path-safe filename component for a (possibly untrusted ``X-Hermes-Session-Id``) session ID.
Collapses non ``[A-Za-z0-9_-]`` chars to ``_``, caps length, and appends a short content hash
whenever sanitization changed the string so distinct IDs cannot collide.
"""
"""Path-safe filename component for a (possibly untrusted ``X-Hermes-Session-Id``) session ID:
non ``[A-Za-z0-9_-]`` → ``_``, capped, plus a content hash when changed so distinct IDs cannot collide."""
raw = str(session_id or "").strip()
sanitized = re.sub(r"[^\w-]", "_", raw).strip("._")
sanitized = sanitized[:96] or "session"
@@ -74,12 +71,9 @@ def _safe_session_filename_component(session_id: str) -> str:
def _override_replaces_content(msg: Dict, content: Any, override: Any) -> bool:
"""Whether the persist user-message override may replace ``content``.
A plain-text override must not replace native image/audio blocks (a list override is the clean
multimodal payload and does). Preflight compaction may re-anchor the index at a message MERGED
with the compaction summary — overwriting it would drop the summary from the durable transcript.
"""
"""Whether the persist user-message override may replace ``content``: a plain-text override must
not replace native image/audio blocks (a list override is the clean multimodal payload and does),
and never a message MERGED with a compaction summary (overwriting would drop the summary)."""
return (
override is not None
and not msg.get(COMPRESSED_SUMMARY_METADATA_KEY)
@@ -88,8 +82,8 @@ def _override_replaces_content(msg: Dict, content: Any, override: Any) -> bool:
def _summary_display_kind(msg: Dict) -> Any:
"""Standalone reference handoffs are always hidden so they never occupy the active user slot in
retry/undo dispatch; merge-into-tail carriers keep their prior visibility."""
"""Standalone handoffs are hidden so they never occupy the active user slot in retry/undo
dispatch; merge-into-tail carriers keep their prior visibility."""
if (
msg.get(COMPRESSED_SUMMARY_METADATA_KEY)
and user_originated_turn_view(msg) is None
@@ -103,8 +97,7 @@ def _summary_display_kind(msg: Dict) -> Any:
def _durable_content(content: Any) -> Any:
"""Text-only projection for the DB: multimodal envelopes become their summary, OpenAI-style part
lists keep text and replace images with ``[screenshot]`` (base64 bloats the DB)."""
"""Text-only DB projection: multimodal envelopes → summary; part lists keep text, images → ``[screenshot]``."""
if _is_multimodal_tool_result(content):
return _multimodal_text_summary(content)
if isinstance(content, list):
@@ -132,8 +125,8 @@ def _tool_calls_data(msg: Dict) -> Any:
def _db_flush_seed_ids(agent) -> set:
"""One-shot ``_flushed_db_message_ids`` seed, honoured only for the same session after a
non-empty flush; translated to markers by the scan and cleared afterwards."""
"""One-shot ``_flushed_db_message_ids`` seed (same session, after a non-empty flush); the scan
translates it to markers and the flush clears it."""
current_session_id = getattr(agent, "session_id", None)
seed_ids = None
if getattr(agent, "_flushed_db_message_session_id", None) == current_session_id and agent._last_flushed_db_idx != 0:
@@ -143,8 +136,7 @@ def _db_flush_seed_ids(agent) -> set:
def _db_flush_scan_start(agent, messages: List[Dict]) -> int:
"""Bounded scan: skip the identity-matched, still-marked prefix of the previous flush's
snapshot — every message in it already got its final disposition."""
"""Skip the identity-matched, still-marked prefix of the previous flush's snapshot."""
scan_start = 0
prev_prefix = getattr(agent, "_db_flush_scan_prefix", None)
if isinstance(prev_prefix, list):
@@ -231,8 +223,7 @@ def _db_flush_collect(agent, messages: List[Dict], conversation_history: Optiona
def _db_flush_write(agent, batch_rows: List[Dict[str, Any]], batch_msgs: List[Dict]) -> None:
"""One transaction for the turn's new rows; on failure no rows land and no markers are
stamped, so the next flush re-writes the tail."""
"""One transaction for the turn's new rows: on failure nothing lands and no markers are stamped."""
if not batch_rows:
return
agent._session_db.append_messages_batch(
@@ -246,11 +237,8 @@ def _db_flush_write(agent, batch_rows: List[Dict[str, Any]], batch_msgs: List[Di
def _db_flush_adopt_compression_tip(agent) -> bool:
"""Adopt the live continuation of a session closed by compression, if there is one.
``get_compression_tip`` returning the same id means no continuation exists; a tip whose row
is missing or already ended is not adopted either.
"""
"""Adopt the live continuation of a session closed by compression. Same-id tip = no continuation;
a tip whose row is missing or already ended is not adopted either."""
old_id = agent.session_id
try:
tip = agent._session_db.get_compression_tip(old_id)
@@ -313,11 +301,8 @@ class SessionPersistenceMixin:
"""Session DB flush, session log and trajectory persistence (see module docstring)."""
def _apply_persist_user_message_override(self, messages: List[Dict]) -> None:
"""Rewrite the current-turn user message in place before persistence/return.
Some paths send an API-only user-message variant that must not leak into transcripts or
resumed history; mutating the live list keeps persistence and returned history clean.
"""
"""Rewrite the current-turn user message in place: some paths send an API-only variant that
must not leak into transcripts or resumed history."""
idx = getattr(self, "_persist_user_message_idx", None)
override = getattr(self, "_persist_user_message_override", None)
timestamp = getattr(self, "_persist_user_message_timestamp", None)
@@ -336,13 +321,9 @@ class SessionPersistenceMixin:
msg["platform_message_id"] = platform_id
def _persist_session(self, messages: List[Dict], conversation_history: List[Dict] = None):
"""Save session state to both JSON log and SQLite on any exit path.
Trailing empty-response scaffolding is dropped from the live list. The persist user-message
override is NOT applied here — ``_flush_messages_to_session_db`` writes it to the DB row only.
"""
# Close and turn-start persistence can run on separate CLI threads, so scaffolding removal
# plus the marker test-and-append must be one critical section.
"""Save session state to both JSON log and SQLite on any exit path. Trailing empty-response
scaffolding is dropped from the live list; the persist override is applied to the DB row only."""
# Close and turn-start persistence can run on separate CLI threads: one critical section.
from agent.agent_runtime_helpers import note_turn_persisted
with getattr(self, "_session_persist_lock", None) or nullcontext():
@@ -356,12 +337,9 @@ class SessionPersistenceMixin:
note_turn_persisted(self)
def _drop_trailing_empty_response_scaffolding(self, messages: List[Dict]) -> None:
"""Remove private empty-response retry/failure scaffolding from transcript tails.
Also rewinds a trailing tool-result / assistant(tool_calls) pair the failed iteration left
hanging; otherwise the next user turn lands as ``...tool, user`` and providers return empty
content forever. The rewind only runs when scaffolding was actually present.
"""
"""Remove empty-response retry scaffolding from the tail, then (only if any was present) rewind
the tool-result / assistant(tool_calls) pair the failed iteration left hanging — otherwise the
next user turn lands as ``...tool, user`` and providers return empty content forever."""
def _tail(*keys: str) -> bool:
return bool(messages) and isinstance(messages[-1], dict) and any(messages[-1].get(k) for k in keys)
@@ -390,15 +368,12 @@ class SessionPersistenceMixin:
conversation_history: Optional[List[Dict]] = None,
_adoption_budget: int = 1,
):
"""Persist any un-flushed messages to the SQLite session store.
Dedup is an intrinsic ``_DB_PERSISTED_MARKER`` on each written dict — not positional slices
(drift after sequence repair) nor a retained ``id(msg)`` set (address reuse). The persist
user-message override is applied ONLY to the written row, never to the live dict. A
compression-closed session adopts its live tip and retries exactly once.
"""
# Persistence-isolated agents (background review fork) share the parent's session_id for
# cache warmth; a write here would land the curator's turn in the user's real history.
"""Persist un-flushed messages to SQLite. Dedup is the intrinsic ``_DB_PERSISTED_MARKER`` on
each written dict — not positional slices (drift after sequence repair) nor an ``id(msg)`` set
(address reuse). The persist override touches the written row only. A compression-closed
session adopts its live tip and retries exactly once."""
# Persistence-isolated agents (background review fork) share the parent's session_id for cache
# warmth; a write here would land the curator's turn in the user's real history.
if getattr(self, "_persist_disabled", False):
return None
if not self._session_db:
@@ -423,8 +398,7 @@ class SessionPersistenceMixin:
return False
def _get_messages_up_to_last_assistant(self, messages: List[Dict]) -> List[Dict]:
"""Messages up to (not including) the last assistant turn — the rollback point when the final
assistant message is incomplete or malformed. All messages when there is none."""
"""Messages before the last assistant turn (rollback point for a malformed final answer); all if none."""
for i in range(len(messages) - 1, -1, -1):
if messages[i].get("role") == "assistant":
return messages[:i]
@@ -457,8 +431,7 @@ class SessionPersistenceMixin:
@staticmethod
def _redact_message_content(content):
"""Redact secrets in str or list-of-parts content; only text fields are touched.
No-op when ``HERMES_REDACT_SECRETS`` disables redaction."""
"""Redact secrets in str or list-of-parts content (text fields only; honours HERMES_REDACT_SECRETS)."""
if isinstance(content, str):
return redact_sensitive_text(content)
if not isinstance(content, list):
@@ -474,8 +447,7 @@ class SessionPersistenceMixin:
return redacted
def _session_log_entry(self, msg: Dict[str, Any]) -> Dict[str, Any]:
"""Copy of ``msg`` with scratchpad tags normalised and credentials redacted
(defence-in-depth; respects HERMES_REDACT_SECRETS)."""
"""Copy of ``msg`` with scratchpad tags normalised and credentials redacted (respects HERMES_REDACT_SECRETS)."""
if msg.get("role") == "assistant" and msg.get("content"):
msg = dict(msg)
msg["content"] = self._clean_session_content(msg["content"])
@@ -485,12 +457,9 @@ class SessionPersistenceMixin:
return msg
def _save_session_log(self, messages: List[Dict[str, Any]] = None):
"""Optional per-session JSON snapshot (``sessions.write_json_snapshots``, default False).
state.db is canonical; this exists for external tooling reading ``session_{sid}.json``.
Rewrites the full list after every persistence point, never overwriting a larger log with
fewer messages (resumed agent with partial history).
"""
"""Optional per-session JSON snapshot (``sessions.write_json_snapshots``, default False) for
external tooling; state.db is canonical. Rewrites the full list after every persistence point,
never overwriting a larger log with fewer messages (resumed agent with partial history)."""
if not getattr(self, "_session_json_enabled", False):
return
messages = messages or self._session_messages

View File

@@ -52,9 +52,9 @@ _TRUTHY = {"1", "true", "yes", "on"}
# kwargs promoted to top-level payload keys; everything else lands under ``extra``.
_TOP_LEVEL_PAYLOAD_KEYS = {"tool_name", "args", "session_id", "parent_session_id"}
# (home, event, matcher, command) tuples wired to the plugin manager in this process. Matcher is in
# the key (one script may register per-tool under one event); home is in the key so multiplexed-
# gateway profiles (each with their own plugin manager) can register identical triples.
# (home, event, matcher, command) wired to the plugin manager in this process. Matcher is in the key
# (one script may register per-tool under one event); home so multiplexed-gateway profiles can register
# identical triples.
_registered: Set[Tuple[str, str, Optional[str], str]] = set()
_registered_lock = threading.Lock()
# Non-POSIX fallback for allowlist read-modify-write. Must be separate from _registered_lock, which
@@ -67,8 +67,7 @@ def _home_key() -> str:
def _forget_home_registrations(registry: Set[tuple], lock: threading.Lock) -> None:
"""Drop the current home's idempotence keys only (shared with outbound webhooks): a force-reload
in profile A must never drop profile B's live registration."""
"""Drop the current home's keys only (shared with outbound webhooks): profile A's reload must not drop B."""
home_key = _home_key()
with lock:
registry.difference_update({k for k in registry if k[0] == home_key})
@@ -212,11 +211,8 @@ def iter_configured_hooks(cfg: Optional[Dict[str, Any]]) -> List[ShellHookSpec]:
def re_register_config_hooks() -> None:
"""Re-register config hooks after a plugin force-reload cleared the manager's hooks.
Only the current home's idempotence keys are cleared (profile A's reload never drops profile
B); allowlisted commands stay allowlisted, so this never re-prompts.
"""
"""Re-register config hooks after a plugin force-reload cleared the manager's hooks. Only the
current home's keys are cleared (profile A's reload never drops profile B); never re-prompts."""
_forget_home_registrations(_registered, _registered_lock)
from hermes_cli.config import load_config
@@ -405,13 +401,10 @@ def _fail_closed_block(spec: ShellHookSpec, reason: str) -> Dict[str, Any]:
def _evaluate_result(spec: ShellHookSpec, r: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Turn a ``_spawn`` diagnostic dict into the hook's contribution (shared by the live callback
and ``run_once``).
Spawn error/timeout fail open unless fail_closed; exit 2 on a blocking event blocks (message
from stdout JSON, then stderr, then default); other non-zero exits warn then parse stdout;
unparseable stdout on a fail_closed hook blocks.
"""
"""``_spawn`` diagnostic dict → the hook's contribution (shared by the live callback and
``run_once``). Spawn error/timeout fail open unless fail_closed; exit 2 on a blocking event blocks
(message from stdout JSON, then stderr, then default); other non-zero exits warn then parse
stdout; unparseable stdout on a fail_closed hook blocks."""
blocking_event = spec.event in _BLOCKING_EVENTS
fail_closed = spec.fail_closed and blocking_event

View File

@@ -226,12 +226,9 @@ def _handle_is_well_formed(handle: Any) -> bool:
class SubagentLifecycleService:
"""Stable public service returned by :attr:`PluginContext.subagent_lifecycle`.
Running children are in-process only. Completed results remain available
until process exit; ``reconnect`` reports that a serialized handle cannot
reconnect after a restart instead of launching work again.
"""
"""Stable public service returned by :attr:`PluginContext.subagent_lifecycle`. Children run
in-process only; completed results stay until process exit, and ``reconnect`` reports that a
serialized handle cannot reconnect after a restart instead of launching work again."""
def __init__(self, parent_agent_resolver: Callable[[], Any]) -> None:
self._parent_agent_resolver = parent_agent_resolver

View File

@@ -19,13 +19,11 @@ from agent.message_content import flatten_message_text
logger = logging.getLogger(__name__)
# (task_name, exception) -> None; surfaces auxiliary failures to the user so silent drops don't
# pile up as NULL titles.
# (task_name, exception) -> None; surfaces auxiliary failures so silent drops don't pile up as NULL titles.
FailureCallback = Callable[[str, BaseException], None]
# (title, source) -> None; source is the persisted provenance (``derived`` / ``llm``). Consumers
# paying a rate-limited remote rename per title (Discord thread, Telegram topic) should act on
# ``llm`` only; a local sidebar wants both.
# (title, source) -> None; source is the persisted provenance (``derived`` / ``llm``). Consumers paying a
# rate-limited remote rename per title (Discord thread, Telegram topic) should act on ``llm`` only.
TitleCallback = Callable[[str, str], None]
# () -> bool, called right before the LLM request; False skips (e.g. the user switched models and
@@ -36,8 +34,7 @@ RuntimeValidator = Callable[[], bool]
MAX_TITLE_INPUT_CHARS = 1000
# Cap on the instant derived title; a raw fragment reads worse the longer it runs.
MAX_DERIVED_TITLE_CHARS = 48
# Answer-shaped guard: a tiny model sometimes answers the user instead of titling; more words
# than this is rejected rather than truncated and stored.
# Answer-shaped guard: a tiny model sometimes answers instead of titling; longer is rejected, not truncated.
_MAX_TITLE_WORDS = 12
_TITLE_PROMPT_TEMPLATE = (
@@ -64,8 +61,7 @@ _TITLE_PROMPT_TEMPLATE = (
_LANGUAGE_RULE_MATCH_USER = "- Write the title in the same language as the user's message."
_LANGUAGE_RULE_PINNED = "- Write the title in {language}."
# Constrains the response to a single title field, removing the "model answered instead of
# titling" failure class.
# Constrains the response to a single title field ("model answered instead of titling" failure class).
_TITLE_RESPONSE_FORMAT = {
"type": "json_schema",
"json_schema": {
@@ -80,8 +76,8 @@ _TITLE_RESPONSE_FORMAT = {
},
}
# Control-tag wrappers around machine-authored content inside a nominal "user" message (ported
# from Codex CLI's RECOGNIZED_CONTROL_WRAPPERS): stripped, and titling continues on what remains.
# Control-tag wrappers around machine-authored content inside a nominal "user" message (Codex CLI's
# RECOGNIZED_CONTROL_WRAPPERS): stripped, titling continues on what remains.
_CONTROL_WRAPPERS = tuple(
(f"<{tag}>", f"</{tag}>")
for tag in (
@@ -91,23 +87,21 @@ _CONTROL_WRAPPERS = tuple(
)
)
# Hermes' own machine-authored openers: a compaction handoff or resumed session must not be
# titled after its scaffolding.
# Hermes' own machine-authored openers: a compaction handoff or resumed session must not be titled after them.
_MACHINE_PREFIXES = (
"[CONTEXT COMPACTION",
LEGACY_SUMMARY_PREFIX,
"[Runtime note:",
"[System note:",
"[SYSTEM]",
# Model-switch marker (tui_gateway.server._MODEL_SWITCH_MARKER_PREFIX, keep in sync). Persisted
# with role="user" because strict providers reject a non-first system message.
# tui_gateway.server._MODEL_SWITCH_MARKER_PREFIX (keep in sync); persisted as role="user" because
# strict providers reject a non-first system message.
"[System: The active model for this chat has changed to ",
)
def _title_config() -> dict:
"""``auxiliary.title_generation`` from config. Lazy read-only import: avoids hermes_cli
circularity and config-migration writes."""
"""``auxiliary.title_generation`` from config (lazy read-only import: no hermes_cli cycle, no migration writes)."""
from hermes_cli.config import load_config_readonly
return ((load_config_readonly() or {}).get("auxiliary") or {}).get("title_generation") or {}
@@ -132,8 +126,7 @@ def _auto_title_enabled() -> bool:
def strip_control_wrappers(text: str) -> str:
"""Remove leading control wrappers, including nested ones, so a slash-command turn reduces to
the prose the user typed (still titleable, unlike a refusal)."""
"""Remove leading control wrappers (nested too) so a slash-command turn reduces to the prose the user typed."""
if not text:
return ""
current = text.strip()
@@ -160,8 +153,7 @@ def strip_control_wrappers(text: str) -> str:
def _summarize_user_message(user_message: str) -> str:
"""Reduce a user turn to the text worth titling: a ``/skill`` invocation embeds the whole
skill body, so parse the scaffolding first, then strip wrappers."""
"""Text worth titling: a ``/skill`` invocation embeds the whole skill body, so describe it first, then strip wrappers."""
if not user_message:
return ""
described = None
@@ -175,8 +167,7 @@ def _summarize_user_message(user_message: str) -> str:
def is_titleable_user_message(user_message: str) -> bool:
"""False for machine-authored openers and turns that reduce to nothing once control
scaffolding is stripped."""
"""False for machine-authored openers and turns that reduce to nothing once scaffolding is stripped."""
if not isinstance(user_message, str) or not user_message.strip():
return False
if user_message.lstrip().startswith(_MACHINE_PREFIXES):
@@ -185,8 +176,7 @@ def is_titleable_user_message(user_message: str) -> bool:
def derive_title(user_message: str) -> Optional[str]:
"""Instant title: first meaningful line trimmed to a word boundary. No model, never fails;
its job is to beat the model to the screen, not on quality."""
"""Instant title: first meaningful line trimmed to a word boundary. No model, never fails."""
line = " ".join(_first_line(_summarize_user_message(user_message)).split())
if not line:
return None
@@ -208,8 +198,8 @@ def _first_line(text: str) -> str:
def _extract_title_text(content: str) -> str:
"""Pull the title out of a model response: strict JSON, then a loose JSON scan, then
first-line prose so a provider ignoring ``response_format`` still titles."""
"""Title from a model response: strict JSON, then a loose JSON scan, then first-line prose
(a provider ignoring ``response_format`` still titles)."""
if not content:
return ""
raw = content.strip()
@@ -273,12 +263,8 @@ def generate_title(
main_runtime: dict = None,
runtime_validator: Optional[RuntimeValidator] = None,
) -> Optional[str]:
"""Generate a session title from the user's opening message alone (waiting for the assistant
made this slow and bought nothing).
``failure_callback`` gets ``(task, exception)`` when the auxiliary call raises;
``runtime_validator`` runs right before the request and False skips silently.
"""
"""Title from the user's opening message alone (waiting for the assistant made this slow and
bought nothing). ``runtime_validator`` runs right before the request; False skips silently."""
if not _auto_title_enabled():
logger.debug("Auto-title skipped: auxiliary.title_generation.enabled=false")
return None
@@ -336,14 +322,12 @@ def _has_upgraded_title(session_db, session_id: str) -> bool:
def _persist_session_title(session_db, session_id, title, *, source, dedupe=True):
"""Persist a title at *source* authority via ``set_auto_title`` (precedence check + write in
one transaction, so a manual ``/title`` is never overwritten).
one transaction, so a manual ``/title`` is never overwritten); None when a higher-authority
title held the row.
``ValueError`` means the unique-title index rejected the name; append ``#N`` via
``ValueError`` = unique-title index rejected the name → append ``#N`` via
``get_next_title_in_lineage``. ``dedupe=False`` re-raises instead: the derived title is on the
turn's critical path, collides constantly ("hi"), and the widening lineage scan is wasted on a
name the model replaces a second later — the background stage picks the collision back up.
Returns the persisted title, or None when a higher-authority title held the row.
critical path, collides constantly ("hi"), and the model replaces it a second later anyway.
"""
auto_fn = getattr(session_db, "set_auto_title", None)
@@ -379,11 +363,8 @@ def apply_instant_title(
user_message: str,
title_callback: Optional[TitleCallback] = None,
) -> Optional[str]:
"""Write the derived title synchronously (cheap enough to run inline).
Returns the title written, or None when nothing was (no usable text, or a title of at least
``derived`` authority exists). Never raises.
"""
"""Write the derived title synchronously (cheap enough to run inline). Returns the title
written, or None (no usable text, or a title of at least ``derived`` authority exists). Never raises."""
if not session_db or not session_id:
return None
try:
@@ -410,13 +391,10 @@ def auto_title_session(
title_callback: Optional[TitleCallback] = None,
runtime_validator: Optional[RuntimeValidator] = None,
) -> None:
"""Generate and store the model title (daemon-thread target).
Skips when the session already carries an ``llm``/``user`` title. Never lets an exception
escape (the default threading excepthook would spray a raw traceback into the terminal); the
canonical trigger is the post-``hermes update`` stale-module window, where lazy imports read
NEW source against OLD cached modules until the process restarts.
"""
"""Generate and store the model title (daemon-thread target); skips sessions already carrying
an ``llm``/``user`` title. Never lets an exception escape (the threading excepthook would spray
a traceback into the terminal); the canonical trigger is the post-``hermes update``
stale-module window, where lazy imports read NEW source against OLD cached modules."""
try:
# A derived title is expected here — upgrading it is the point.
if not session_db or not session_id or _has_upgraded_title(session_db, session_id):
@@ -464,8 +442,7 @@ def auto_title_session(
def _is_real_user_turn(message: Any) -> bool:
"""Whether a history entry is a question a person actually asked (Hermes persists machinery
under ``role="user"``); a multimodal turn is judged on its text."""
"""A question a person actually asked (Hermes persists machinery under ``role="user"``)."""
if not isinstance(message, dict) or message.get("role") != "user":
return False
content = message.get("content")
@@ -473,8 +450,7 @@ def _is_real_user_turn(message: Any) -> bool:
def _session_is_untitled(session_db, session_id: str) -> bool:
"""Whether the session carries no title of any provenance. False when it can't tell: an
unreadable title is no reason to spend a model call per turn."""
"""No title of any provenance. False when it can't tell — no model call per turn for an unreadable title."""
getter = getattr(session_db, "get_session_title", None)
if not callable(getter):
return False
@@ -495,14 +471,13 @@ def maybe_auto_title(
title_callback: Optional[TitleCallback] = None,
runtime_validator: Optional[RuntimeValidator] = None,
) -> None:
"""Title a session from its opening message: instant inline, then upgraded on a daemon
thread. Call at the START of a turn, before the model is invoked."""
"""Title a session from its opening message: instant inline, then upgraded on a daemon thread.
Call at the START of a turn, before the model is invoked."""
if not session_db or not session_id or not user_message:
return
# History may be pre- or post-message depending on the caller. Skip only when BOTH past the
# opening turn AND already named: count alone left a session that opened with machinery
# nameless; title alone never titles on a store too old to report one.
# History may be pre- or post-message. Skip only when BOTH past the opening turn AND already named:
# count alone left a machinery-opened session nameless; title alone never titles on an old store.
user_msg_count = sum(1 for m in (conversation_history or []) if _is_real_user_turn(m))
if user_msg_count > 1 and not _session_is_untitled(session_db, session_id):
return

View File

@@ -171,13 +171,9 @@ def build_trace_jsonl(
cwd: str = "",
redact: bool = True,
) -> str:
"""Render Hermes conversation messages as Claude Code JSONL text.
Each non-system message becomes one line: ``user``/``tool`` -> ``{"type": "user"}``,
``assistant`` -> ``{"type": "assistant"}`` with text + ``tool_use`` blocks. Tool results ride
on user turns as a ``tool_result`` block keyed by ``tool_call_id``; turns link via ``uuid`` /
``parentUuid``.
"""
"""Render messages as Claude Code JSONL: one line per non-system message (``user``/``tool`` ->
type user, ``assistant`` -> type assistant with text + ``tool_use`` blocks; tool results ride
on user turns as ``tool_result`` keyed by ``tool_call_id``; turns link via ``parentUuid``)."""
lines: List[str] = []
parent: Optional[str] = None
base_ts = _now_iso()