diff --git a/agent/replay_cleanup.py b/agent/replay_cleanup.py index b4f17f1a14..b23e8bddcd 100644 --- a/agent/replay_cleanup.py +++ b/agent/replay_cleanup.py @@ -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 diff --git a/agent/session_persistence.py b/agent/session_persistence.py index 3d2adde9cf..d81aee0da3 100644 --- a/agent/session_persistence.py +++ b/agent/session_persistence.py @@ -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 diff --git a/agent/shell_hooks.py b/agent/shell_hooks.py index 72ad15fdb0..5592404131 100644 --- a/agent/shell_hooks.py +++ b/agent/shell_hooks.py @@ -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 diff --git a/agent/subagent_lifecycle.py b/agent/subagent_lifecycle.py index ad97344bd3..4db2592af4 100644 --- a/agent/subagent_lifecycle.py +++ b/agent/subagent_lifecycle.py @@ -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 diff --git a/agent/title_generator.py b/agent/title_generator.py index 3d9b5bfa82..1235cc9c2b 100644 --- a/agent/title_generator.py +++ b/agent/title_generator.py @@ -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"") 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 diff --git a/agent/trace_upload.py b/agent/trace_upload.py index c4bed71177..345e1189d9 100644 --- a/agent/trace_upload.py +++ b/agent/trace_upload.py @@ -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()