# NousResearch/hermes-agent · Honcho memory gateway scoping and lifecycle fixes

## Open now

- Nothing open on GitHub.

These issues and pull requests concern bugs and fixes related to how Honcho memory scopes sessions by gateway user ID, particularly when peer_name is configured, as well as dialectic lifecycle behavior including defaults, retry, and prewarm handling.

## TL;DR

- 5 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #12419. Verify it addresses your case.
- **Recommended:** read PR #12419 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-12419`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.34`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #11199: \[Bug\]: Honcho memory scoping ignores gateway user_id when peer_name is configured
- #14401: Honcho memory plugin ignores gateway user_id when peer_name is configured

## Prior & competing PRs

- **#12419: fix(honcho): dialectic lifecycle, gateway scoping, provider opt-in**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `optional-skills/autonomous-ai-agents/honcho/SKILL.md`, `plugins/memory/honcho/__init__.py`, `plugins/memory/honcho/cli.py`, `plugins/memory/honcho/client.py`, `plugins/memory/honcho/session.py`, `run_agent.py`, `scripts/release.py`, `tests/agent/test_memory_provider.py`, `tests/agent/test_memory_user_id.py`, `tests/honcho_plugin/test_async_memory.py`, `tests/honcho_plugin/test_cli.py`, `tests/honcho_plugin/test_session.py`, `tests/run_agent/test_memory_provider_init.py`, `website/docs/user-guide/features/honcho.md`, `website/docs/user-guide/features/memory-providers.md`
  - fix hunks:
  - `optional-skills/autonomous-ai-agents/honcho/SKILL.md`

    ```diff
    @@ -145,10 +145,10 @@ Controls **how often** dialectic and context calls happen.
     | Key | Default | Description |
     |-----|---------|-------------|
     | `contextCadence` | `1` | Min turns between context API calls |
    -| `dialecticCadence` | `3` | Min turns between dialectic API calls |
    +| `dialecticCadence` | `2` | Min turns between dialectic API calls. Recommended 1–5 |
     | `injectionFrequency` | `every-turn` | `every-turn` or `first-turn` for base context injection |
     
    -Higher cadence values reduce API calls and cost. `dialecticCadence: 3` (default) means the dialectic engine fires at most every 3rd turn.
    +Higher cadence values fire the dialectic LLM less often. `dialecticCadence: 2` means the engine fires every other turn. Setting it to `1` fires every turn.
     
     ### Depth (how many)
     
    ```

    ```diff
    @@ -180,6 +180,8 @@ If `dialecticDepthLevels` is omitted, rounds use **proportional levels** derived
     
     This keeps earlier passes cheap while using full depth on the final synthesis.
     
    +**Depth at session start.** The session-start prewarm runs the full configured `dialecticDepth` in the background before turn 1. A single-pass prewarm on a cold peer often returns thin output — multi-pass depth runs the audit/reconcile cycle before the user ever speaks. Turn 1 consumes the prewarm result directly; if prewarm hasn't landed in time, turn 1 falls back to a synchronous call with a bounded timeout.
    +
     ### Level (how hard)
     
     Controls the **intensity** of each dialectic reasoning round.
    ```

    ```diff
    @@ -368,7 +370,7 @@ Config file: `$HERMES_HOME/honcho.json` (profile-local) or `~/.honcho/config.jso
     | `contextTokens` | uncapped | Max tokens for the combined base context injection (summary + representation + card). Opt-in cap — omit to leave uncapped, set to an integer to bound injection size. |
     | `injectionFrequency` | `every-turn` | `every-turn` or `first-turn` |
     | `contextCadence` | `1` | Min turns between context API calls |
    -| `dialecticCadence` | `3` | Min turns between dialectic LLM calls |
    +| `dialecticCadence` | `2` | Min turns between dialectic LLM calls (recommended 1–5) |
     
     The `contextTokens` budget is enforced at injection time. If the session summary + representation + card exceed the budget, Honcho trims the summary first, then the representation, preserving the card. This prevents context blowup in long sessions.
     
    ```
  - `plugins/memory/honcho/__init__.py`

    ```diff
    @@ -19,6 +19,7 @@
     import logging
     import re
     import threading
    +import time
     from typing import Any, Dict, List, Optional
     
     from agent.memory_provider import MemoryProvider
    ```

    ```diff
    @@ -206,13 +207,19 @@ def __init__(self):
             self._turn_count = 0
             self._injection_frequency = "every-turn"  # or "first-turn"
             self._context_cadence = 1   # minimum turns between context API calls
    -        self._dialectic_cadence = 3  # minimum turns between dialectic API calls
    +        self._dialectic_cadence = 1  # backwards-compat fallback; wizard writes 2 on new configs
             self._dialectic_depth = 1   # how many .chat() calls per dialectic cycle (1-3)
             self._dialectic_depth_levels: list[str] | None = None  # per-pass reasoning levels
    -        self._reasoning_level_cap: Optional[str] = None  # "minimal", "low", "medium", "high"
    +        self._reasoning_heuristic: bool = True  # scale base level by query length
    +        self._reasoning_level_cap: str = "high"  # ceiling for auto-selected level
             self._last_context_turn = -999
             self._last_dialectic_turn = -999
     
    +        # Liveness + observability state
    +        self._prefetch_thread_started_at: float = 0.0   # monotonic ts of current thread
    +        self._prefetch_result_fired_at: int = -999      # turn the pending result was fired at
    +        self._dialectic_empty_streak: int = 0           # consecutive empty returns
    +
             # Port #1957: lazy session init for tools-only mode
             self._session_initialized = False
             self._lazy_init_kwargs: Optional[dict] = None
    ```

    ```diff
    @@ -286,14 +293,6 @@ def initialize(self, session_id: str, **kwargs) -> None:
                     logger.debug("Honcho not configured — plugin inactive")
                     return
     
    -            # Override peer_name with gateway user_id for per-user memory scoping.
    -            # Only when no explicit peerName was configured — an explicit peerName
    -            # means the user chose their identity; a raw user_id (e.g. Telegram
    -            # chat ID) should not silently replace it.
    -            _gw_user_id = kwargs.get("user_id")
    -            if _gw_user_id and not cfg.peer_name:
    -                cfg.peer_name = _gw_user_id
    -
                 self._config = cfg
     
                 # ----- B1: recall_mode from config -----
    ```

    ```diff
    @@ -305,12 +304,16 @@ def initialize(self, session_id: str, **kwargs) -> None:
                     raw = cfg.raw or {}
                     self._injection_frequency = raw.get("injectionFrequency", "every-turn")
                     self._context_cadence = int(raw.get("contextCadence", 1))
    -                self._dialectic_cadence = int(raw.get("dialecticCadence", 3))
    +                # Backwards-compat: unset dialecticCadence falls back to 1
    +                # (every turn) so existing honcho.json configs without the key
    +                # behave as they did before. New setups via `hermes honcho setup`
    +                # get dialecticCadence=2 written explicitly by the wizard.
    +                self._dialectic_cadence = int(raw.get("dialecticCadence", 1))
                     self._dialectic_depth = max(1, min(cfg.dialectic_depth, 3))
                     self._dialectic_depth_levels = cfg.dialectic_depth_levels
    -                cap = raw.get("reasoningLevelCap")
    -                if cap and cap in ("minimal", "low", "medium", "high"):
    -                    self._reasoning_level_cap = cap
    +                self._reasoning_heuristic = cfg.reasoning_heuristic
    +                if cfg.reasoning_level_cap in self._LEVEL_ORDER:
    +                    self._reasoning_level_cap = cfg.reasoning_level_cap
                 except Exception as e:
                     logger.debug("Honcho cost-awareness config parse error: %s", e)
     
    ```

    ```diff
    @@ -352,6 +355,7 @@ def _do_session_init(self, cfg, session_id: str, **kwargs) -> None:
                 honcho=client,
                 config=cfg,
                 context_tokens=[redacted:secret-assignment]
    +            runtime_user_peer_name=kwargs.get("user_id") or None,
             )
     
             # ----- B3: resolve_session_name -----
    ```

    ```diff
    @@ -391,14 +395,45 @@ def _do_session_init(self, cfg, session_id: str, **kwargs) -> None:
             except Exception as e:
                 logger.debug("Honcho memory file migration skipped: %s", e)
     
    -        # ----- B7: Pre-warming context at init -----
    +        # ----- B7: Pre-warming at init -----
    +        # Context prewarm warms peer.context() (base layer), consumed via
    +        # pop_context_result() in prefetch(). Dialectic prewarm runs the
    +        # full configured depth and writes into _prefetch_result so turn 1
    +        # consumes the result directly.
             if self._recall_mode in ("context", "hybrid"):
                 try:
                     self._manager.prefetch_context(self._session_key)
    -                self._manager.prefetch_dialectic(self._session_key, "What should I know about this user?")
    -                logger.debug("Honcho pre-warm threads started for session: %s", self._session_key)
                 except Exception as e:
    -                logger.debug("Honcho pre-warm failed: %s", e)
    +                logger.debug("Honcho context prewarm failed: %s", e)
    +
    +            _prewarm_query = (
    +                "Summarize what you know about this user. "
    +                "Focus on preferences, current projects, and working style."
    +            )
    +
    +            def _prewarm_dialectic() -> None:
    +                try:
    +                    r = self._run_dialectic_depth(_prewarm_query)
    +                except Exception as exc:
    +                    logger.debug("Honcho dialectic prewarm failed: %s", exc)
    +                    self._dialectic_empty_streak += 1
    +                    return
    +                if r and r.strip():
    +                    with self._prefetch_lock:
    +                        self._prefetch_result = r
    +                        self._prefetch_result_fired_at = 0
    +                    # Treat prewarm as turn 0 so cadence gating starts clean.
    +                    self._last_dialectic_turn = 0
    +                    self._dialectic_empty_streak = 0
    +                else:
    +                    self._dialectic_empty_streak += 1
    +
    +            self._prefetch_thread_started_at = time.monotonic()
    +            self._prefetch_thread = threading.Thread(
    +                target=_prewarm_dialectic, daemon=True, name="honcho-prewarm-dialectic"
    +            )
    +            self._prefetch_thread.start()
    +            logger.debug("Honcho pre-warm started for session: %s", self._session_key)
     
         def _ensure_session(self) -> bool:
             """Lazily initialize the Honcho session (for tools-only mode).
    ```

    ```diff
    @@ -487,7 +522,8 @@ def system_prompt_block(self) -> str:
                     "# Honcho Memory\n"
                     "Active (tools-only mode). Use honcho_profile for a quick factual snapshot, "
                     "honcho_search for raw excerpts, honcho_context for raw peer context, "
    -                "honcho_reasoning for synthesized answers, "
    +                "honcho_reasoning for synthesized answers (pass reasoning_level "
    +                "minimal/low/medium/high/max — you pick the depth per call), "
                     "honcho_conclude to save facts about the user. "
                     "No automatic context injection — you must use tools to access memory."
                 )
    ```

    ```diff
    @@ -497,7 +533,8 @@ def system_prompt_block(self) -> str:
                     "Active (hybrid mode). Relevant context is auto-injected AND memory tools are available. "
                     "Use honcho_profile for a quick factual snapshot, "
                     "honcho_search for raw excerpts, honcho_context for raw peer context, "
    -                "honcho_reasoning for synthesized answers, "
    +                "honcho_reasoning for synthesized answers (pass reasoning_level "
    +                "minimal/low/medium/high/max — you pick the depth per call), "
                     "honcho_conclude to save facts about the user."
                 )
     
    ```

    ```diff
    @@ -526,6 +563,10 @@ def prefetch(self, query: str, *, session_id: str = "") -> str:
             if self._injection_frequency == "first-turn" and self._turn_count > 1:
                 return ""
     
    +        # Trivial prompts ("ok", "yes", slash commands) carry no semantic signal.
    +        if self._is_trivial_prompt(query):
    +            return ""
    +
             parts = []
     
             # ----- Layer 1: Base context (representation + card) -----
    ```

    ```diff
    @@ -560,43 +601,72 @@ def prefetch(self, query: str, *, session_id: str = "") -> str:
             # On the very first turn, no queue_prefetch() has run yet so the
             # dialectic result is empty.  Run with a bounded timeout so a slow
             # Honcho connection doesn't block the first response indefinitely.
    -        # On timeout the result is skipped and queue_prefetch() will pick it
    -        # up at the next cadence-allowed turn.
    +        # On timeout we let the thread keep running and write its result into
    +        # _prefetch_result under the lock, so the next turn picks it up.
    +        #
    +        # Skip if the session-start prewarm already filled _prefetch_result —
    +        # firing another .chat() would be duplicate work.
    +        with self._prefetch_lock:
    +            _prewarm_landed = bool(self._prefetch_result)
    +        if _prewarm_landed and self._last_dialectic_turn == -999:
    +            self._last_dialectic_turn = self._turn_count
    +
             if self._last_dialectic_turn == -999 and query:
                 _first_turn_timeout = (
                     self._config.timeout if self._config and self._config.timeout else 8.0
                 )
    -            _result_holder: list[str] = []
    +            _fired_at = self._turn_count
     
                 def _run_first_turn() -> None:
                     try:
    -                    _result_holder.append(self._run_dialectic_depth(query))
    +                    r = self._run_dialectic_depth(query)
                     except Exception as exc:
                         logger.debug("Honcho first-turn dialectic failed: %s", exc)
    -
    -            _t = threading.Thread(target=_run_first_turn, daemon=True)
    -            _t.start()
    -            _t.join(timeout=_first_turn_timeout)
    -            if not _t.is_alive():
    -                first_turn_dialectic = _result_holder[0] if _result_holder else ""
    -                if first_turn_dialectic and first_turn_dialectic.strip():
    +                    self._dialectic_empty_streak += 1
    +                    return
    +                if r and r.strip():
                         with self._prefetch_lock:
    -                        self._prefetch_result = first_turn_dialectic
    -                self._last_dialectic_turn = self._turn_count
    -            else:
    +                        self._prefetch_result = r
    +                        self._prefetch_result_fired_at = _fired_at
    +                    # Advance cadence only on a non-empty result so the next
    +                    # turn retries when the call returned nothing.
    +                    self._last_dialectic_turn = _fired_at
    +                    self._dialectic_empty_streak = 0
    +                else:
    +                    self._dialectic_empty_streak += 1
    +
    +            self._prefetch_thread_started_at = time.monotonic()
    +            self._prefetch_thread = threading.Thread(
    +                target=_run_first_turn, daemon=True, name="honcho-prefetch-first"
    +            )
    +            self._prefetch_thread.start()
    +            self._prefetch_thread.join(timeout=_first_turn_timeout)
    +            if self._prefetch_thread.is_alive():
                     logger.debug(
    -                    "Honcho first-turn dialectic timed out (%.1fs) — "
    -                    "will inject at next cadence-allowed turn",
    +                    "Honcho first-turn dialectic still running after %.1fs — "
    +                    "will surface on next turn",
                         _first_turn_timeout,
                     )
    -                # Don't update _last_dialectic_turn: queue_prefetch() will
    -                # retry at the next cadence-allowed turn via the async path.
     
             if self._prefetch_thread and self._prefetch_thread.is_alive():
                 self._prefetch_thread.join(timeout=3.0)
             with self._prefetch_lock:
                 dialectic_result = self._prefetch_result
    +            fired_at = self._prefetch_result_fired_at
                 self._prefetch_result = ""
    +            self._prefetch_result_fired_at = -999
    +
    +        # Discard stale pending results: if the fire happened more than
    +        # cadence × multiplier turns ago (e.g. a run of trivial-prompt turns
    +        # passed without consumption), the content likely no longer tracks
    +        # the current conversational pivot.
    +        stale_limit = self._dialectic_cadence * self._STALE_RESULT_MULTIPLIER
    +        if dialectic_result and fired_at >= 0 and (self._turn_count - fired_at) > stale_limit:
    +            logger.debug(
    +                "Honcho pending dialectic discarded as stale: fired_at=%d, "
    +                "turn=%d, limit=%d", fired_at, self._turn_count, stale_limit,
    +            )
    +            dialectic_result = ""
     
             if dialectic_result and dialectic_result.strip():
                 parts.append(dialectic_result)
    ```

    ```diff
    @@ -641,6 +711,10 @@ def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
             if self._recall_mode == "tools":
                 return
     
    +        # Trivial prompts don't warrant either a context refresh or a dialectic call.
    +        if self._is_trivial_prompt(query):
    +            return
    +
             # ----- Context refresh (base layer) — independent cadence -----
             if self._context_cadence <= 1 or (self._turn_count - self._last_context_turn) >= self._context_cadence:
                 self._last_context_turn = self._turn_count
    ```

    ```diff
    @@ -650,24 +724,46 @@ def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
                     logger.debug("Honcho context prefetch failed: %s", e)
     
             # ----- Dialectic prefetch (supplement layer) -----
    -        # B5: cadence check — skip if too soon since last dialectic call
    -        if self._dialectic_cadence > 1:
    -            if (self._turn_count - self._last_dialectic_turn) < self._dialectic_cadence:
    -                logger.debug("Honcho dialectic prefetch skipped: cadence %d, turns since last: %d",
    -                             self._dialectic_cadence, self._turn_count - self._last_dialectic_turn)
    -                return
    +        # Thread-alive guard with stale-thread recovery: a hung Honcho call
    +        # older than timeout × multiplier is treated as dead so it can't
    +        # block subsequent fires.
    +        if self._thread_is_live():
    +            logger.debug("Honcho dialectic prefetch skipped: prior thread still running")
    +            return
     
    -        self._last_dialectic_turn = self._turn_count
    +        # Cadence gate, widened by the empty-streak backoff so a persistently
    +        # silent backend doesn't retry every turn forever.
    +        effective = self._effective_cadence()
    +        if (self._turn_count - self._last_dialectic_turn) < effective:
    +            logger.debug(
    +                "Honcho dialectic prefetch skipped: effective cadence %d "
    +                "(base %d, empty streak %d), turns since last: %d",
    +                effective, self._dialectic_cadence, self._dialectic_empty_streak,
    +                self._turn_count - self._last_dialectic_turn,
    +            )
    +            return
    +
    +        # Cadence advances only on a non-empty result so empty returns
    +        # (transient API error, sparse representation) retry next turn.
    +        _fired_at = self._turn_count
     
             def _run():
                 try:
                     result = self._run_dialectic_depth(query)
    -                if result and result.strip():
    -                    with self._prefetch_lock:
    -                        self._prefetch_result = result
                 except Exception as e:
                     logger.debug("Honcho prefetch failed: %s", e)
    +                self._dialectic_empty_streak += 1
    +                return
    +            if result and result.strip():
    +                with self._prefetch_lock:
    +                    self._prefetch_result = result
    +                    self._prefetch_result_fired_at = _fired_at
    +                self._last_dialectic_turn = _fired_at
    +                self._dialectic_empty_streak = 0
    +            else:
    +                self._dialectic_empty_streak += 1
     
    +        self._prefetch_thread_started_at = time.monotonic()
             self._prefetch_thread = threading.Thread(
                 target=_run, daemon=True, name="honcho-prefetch"
             )
    ```

    ```diff
    @@ -692,11 +788,91 @@ def _run():
     
         _LEVEL_ORDER = ("minimal", "low", "medium", "high", "max")
     
    -    def _resolve_pass_level(self, pass_idx: int) -> str:
    +    # Char-count thresholds for the query-length reasoning heuristic.
    +    _HEURISTIC_LENGTH_MEDIUM = 120
    +    _HEURISTIC_LENGTH_HIGH = 400
    +
    +    # Liveness constants. A thread older than timeout × multiplier is treated
    +    # as dead so a hung Honcho call can't block future retries indefinitely.
    +    _STALE_THREAD_MULTIPLIER = 2.0
    +    # Pending result whose fire-turn is older than cadence × multiplier is
    +    # discarded on read so we don't inject context for a stale conversational
    +    # pivot after a gap of trivial-prompt turns.
    +    _STALE_RESULT_MULTIPLIER = 2
    +    # Cap on the empty-streak backoff so a persistently silent backend
    +    # eventually settles on a ceiling instead of unbounded widening.
    +    _BACKOFF_MAX = 8
    +
    +    def _thread_is_live(self) -> bool:
    +        """Thread-alive guard that treats threads older than the stale
    +        threshold as dead, so a hung Honcho request can't block new fires."""
    +        if not self._prefetch_thread or not self._prefetch_thread.is_alive():
    +            return False
    +        timeout = (self._config.timeout if self._config and self._config.timeout else 8.0)
    +        age = time.monotonic() - self._prefetch_thread_started_at
    +        if age > timeout * self._STALE_THREAD_MULTIPLIER:
    +            logger.debug(
    +                "Honcho prefetch thread age %.1fs exceeds stale threshold "
    +                "%.1fs — treating as dead", age, timeout * self._STALE_THREAD_MULTIPLIER,
    +            )
    +            return False
    +        return True
    +
    +    def _effective_cadence(self) -> int:
    +        """Cadence plus empty-streak backoff, capped at _BACKOFF_MAX × base."""
    +        if self._dialectic_empty_streak <= 0:
    +            return self._dialectic_cadence
    +        widened = self._dialectic_cadence + self._dialectic_empty_streak
    +        ceiling = self._dialectic_cadence * self._BACKOFF_MAX
    +        return min(widened, ceiling)
    +
    +    def liveness_snapshot(self) -> dict:
    +        """In-process snapshot of dialectic liveness state for diagnostics.
    +
    +        Returns current turn, last successful dialectic turn, pending-result
    +        fire turn, empty streak, effective cadence, and thread status.
    +        """
    +        thread_age = None
    +        if self._prefetch_thread and self._prefetch_thread.is_alive():
    +            thread_age = time.monotonic() - self._prefetch_thread_started_at
    +        return {
    +            "turn_count": self._turn_count,
    +            "last_dialectic_turn": self._last_dialectic_turn,
    +            "pending_result_fired_at": self._prefetch_result_fired_at,
    +            "empty_streak": self._dialectic_empty_streak,
    +            "effective_cadence": self._effective_cadence(),
    +            "thread_alive": thread_age is not None,
    +            "thread_age_seconds": thread_age,
    +        }
    +
    +    def _apply_reasoning_heuristic(self, base: str, query: str) -> str:
    +        """Scale `base` up by query length, clamped at reasoning_level_cap.
    +
    +        Char-count heuristic: +1 at >=120 chars, +2 at >=400.
    +        """
    +        if not self._reasoning_heuristic or not query:
    +            return base
    +        if base not in self._LEVEL_ORDER:
    +            return base
    +        n = len(query)
    +        if n < self._HEURISTIC_LENGTH_MEDIUM:
    +            bump = 0
    +        elif n < self._HEURISTIC_LENGTH_HIGH:
    +            bump = 1
    +        else:
    +            bump = 2
    +        base_idx = self._LEVEL_ORDER.index(base)
    +        cap_idx = self._LEVEL_ORDER.index(self._reasoning_level_cap)
    +        return self._LEVEL_ORDER[min(base_idx + bump, cap_idx)]
    +
    +    def _resolve_pass_level(self, pass_idx: int, query: str = "") -> str:
             """Resolve reasoning level for a given pass index.
     
    -        Uses dialecticDepthLevels if configured, otherwise proportional
    -        defaults relative to dialecticReasoningLevel.
    +        Precedence:
    +          1. dialecticDepthLevels (explicit per-pass) — wins absolutely
    +          2. _PROPORTIONAL_LEVELS table (depth>1 lighter-early passes)
    +          3. Base level = dialecticReasoningLevel, optionally scaled by the
    +             reasoning heuristic when the mapping falls through to 'base'
             """
             if self._dialectic_depth_levels and pass_idx < len(self._dialectic_depth_levels):
                 return self._dialectic_depth_levels[pass_idx]
    ```

    ```diff
    @@ -704,7 +880,7 @@ def _resolve_pass_level(self, pass_idx: int) -> str:
             base = (self._config.dialectic_reasoning_level if self._config else "low")
             mapping = self._PROPORTIONAL_LEVELS.get((self._dialectic_depth, pass_idx))
             if mapping is None or mapping == "base":
    -            return base
    +            return self._apply_reasoning_heuristic(base, query)
             return mapping
     
         def _build_dialectic_prompt(self, pass_idx: int, prior_results: list[str], is_cold: bool) -> str:
    ```

    ```diff
    @@ -791,7 +967,7 @@ def _run_dialectic_depth(self, query: str) -> str:
                         break
                     prompt = self._build_dialectic_prompt(i, results, is_cold)
     
    -            level = self._resolve_pass_level(i)
    +            level = self._resolve_pass_level(i, query=query)
                 logger.debug("Honcho dialectic depth %d: pass %d, level=%s, cold=%s",
                              self._dialectic_depth, i, level, is_cold)
     
    ```

    ```diff
    @@ -808,6 +984,29 @@ def _run_dialectic_depth(self, query: str) -> str:
                     return r
             return ""
     
    +    # Prompts that carry no semantic signal — trivial acknowledgements, slash
    +    # commands, empty input. Skipping injection here saves tokens and prevents
    +    # stale user-model context from derailing one-word replies.
    +    _TRIVIAL_PROMPT_RE = re.compile(
    +        r'^(yes|no|ok|okay|sure|thanks|thank you|y|n|yep|nope|yeah|nah|'
    +        r'continue|go ahead|do it|proceed|got it|cool|nice|great|done|next|lgtm|k)$',
    +        re.IGNORECASE,
    +    )
    +
    +    @classmethod
    +    def _is_trivial_prompt(cls, text: str) -> bool:
    +        """Return True if the prompt is too trivial to warrant context injection."""
    +        if not text:
    +            return True
    +        stripped = text.strip()
    +        if not stripped:
    +            return True
    +        if stripped.startswith("/"):
    +            return True
    +        if cls._TRIVIAL_PROMPT_RE.match(stripped):
    +            return True
    +        return False
    +
         def on_turn_start(self, turn_number: int, message: str, **kwargs) -> None:
             """Track turn count for cadence and injection_frequency logic."""
             self._turn_count = turn_number
    ```
  - `plugins/memory/honcho/cli.py`

    ```diff
    @@ -460,17 +460,37 @@ def cmd_setup(args) -> None:
                 pass  # keep current
     
         # --- 7b. Dialectic cadence ---
    -    current_dialectic = str(hermes_host.get("dialecticCadence") or cfg.get("dialecticCadence") or "3")
    +    current_dialectic = str(hermes_host.get("dialecticCadence") or cfg.get("dialecticCadence") or "2")
         print("\n  Dialectic cadence:")
         print("    How often Honcho rebuilds its user model (LLM call on Honcho backend).")
    -    print("    1 = every turn (aggressive), 3 = every 3 turns (recommended), 5+ = sparse.")
    +    print("    1 = every turn, 2 = every other turn, 3+ = sparser.")
    +    print("    Recommended: 1-5.")
         new_dialectic = _prompt("Dialectic cadence", default=current_dialectic)
         try:
             val = int(new_dialectic)
             if val >= 1:
                 hermes_host["dialecticCadence"] = val
         except (ValueError, TypeError):
    -        hermes_host["dialecticCadence"] = 3
    +        hermes_host["dialecticCadence"] = 2
    +
    +    # --- 7c. Dialectic reasoning level ---
    +    current_reasoning = (
    +        hermes_host.get("dialecticReasoningLevel")
    +        or cfg.get("dialecticReasoningLevel")
    +        or "low"
    +    )
    +    print("\n  Dialectic reasoning level:")
    +    print("    Depth Honcho uses when synthesizing user context on auto-injected calls.")
    +    print("    minimal  -- quick factual lookups")
    +    print("    low      -- straightforward questions (default)")
    +    print("    medium   -- multi-aspect synthesis")
    +    print("    high     -- complex behavioral patterns")
    +    print("    max      -- thorough audit-level analysis")
    +    new_reasoning = _prompt("Reasoning level", default=current_reasoning)
    +    if new_reasoning in ("minimal", "low", "medium", "high", "max"):
    +        hermes_host["dialecticReasoningLevel"] = new_reasoning
    +    else:
    +        hermes_host["dialecticReasoningLevel"] = "low"
     
         # --- 8. Session strategy ---
         current_strat = hermes_host.get("sessionStrategy") or cfg.get("sessionStrategy", "per-session")
    ```

    ```diff
    @@ -636,8 +656,11 @@ def cmd_status(args) -> None:
         print(f"  Recall mode:    {hcfg.recall_mode}")
         print(f"  Context budget: {hcfg.context_tokens or '(uncapped)'} tokens")
         raw = getattr(hcfg, "raw", None) or {}
    -    dialectic_cadence = raw.get("dialecticCadence") or 3
    +    dialectic_cadence = raw.get("dialecticCadence") or 1
         print(f"  Dialectic cad:  every {dialectic_cadence} turn{'s' if dialectic_cadence != 1 else ''}")
    +    reasoning_cap = raw.get("reasoningLevelCap") or hcfg.reasoning_level_cap
    +    heuristic_on = "on" if hcfg.reasoning_heuristic else "off"
    +    print(f"  Reasoning:      base={hcfg.dialectic_reasoning_level}, cap={reasoning_cap}, heuristic={heuristic_on}")
         print(f"  Observation:    user(me={hcfg.user_observe_me},others={hcfg.user_observe_others}) ai(me={hcfg.ai_observe_me},others={hcfg.ai_observe_others})")
         print(f"  Write freq:     {hcfg.write_frequency}")
     
    ```
  - `plugins/memory/honcho/client.py`

    ```diff
    @@ -251,6 +251,11 @@ class HonchoClientConfig:
         # matching dialectic_depth length. When None, uses proportional defaults
         # derived from dialectic_reasoning_level.
         dialectic_depth_levels: list[str] | None = None
    +    # When true, the auto-injected dialectic scales reasoning level up on
    +    # longer queries. See HonchoMemoryProvider for thresholds.
    +    reasoning_heuristic: bool = True
    +    # Ceiling for the heuristic-selected reasoning level.
    +    reasoning_level_cap: str = "high"
         # Honcho API limits — configurable for self-hosted instances
         # Max chars per message sent via add_messages() (Honcho cloud: 25000)
         message_max_chars: int = 25000
    ```

    ```diff
    @@ -446,6 +451,16 @@ def from_global_config(
                     raw.get("dialecticDepthLevels"),
                     depth=_parse_dialectic_depth(host_block.get("dialecticDepth"), raw.get("dialecticDepth")),
                 ),
    +            reasoning_heuristic=_resolve_bool(
    +                host_block.get("reasoningHeuristic"),
    +                raw.get("reasoningHeuristic"),
    +                default=True,
    +            ),
    +            reasoning_level_cap=(
    +                host_block.get("reasoningLevelCap")
    +                or raw.get("reasoningLevelCap")
    +                or "high"
    +            ),
                 message_max_chars=int(
                     host_block.get("messageMaxChars")
                     or raw.get("messageMaxChars")
    ```
  - `plugins/memory/honcho/session.py`

    ```diff
    @@ -78,6 +78,7 @@ def __init__(
             honcho: Honcho | None = None,
             context_tokens: int | None = None,
             config: Any | None = None,
    +        runtime_user_peer_name: str | None = None,
         ):
             """
             Initialize the session manager.
    ```

    ```diff
    @@ -87,10 +88,12 @@ def __init__(
                 context_tokens: Max tokens for context() calls (None = Honcho default).
                 config: HonchoClientConfig from global config (provides peer_name, ai_peer,
                         write_frequency, observation, etc.).
    +            runtime_user_peer_name: Gateway user identity for per-user memory scoping.
             """
             self._honcho = honcho
             self._context_tokens = [redacted:secret-assignment]
             self._config = config
    +        self._runtime_user_peer_name = runtime_user_peer_name
             self._cache: dict[str, HonchoSession] = {}
             self._peers_cache: dict[str, Any] = {}
             self._sessions_cache: dict[str, Any] = {}
    ```

    ```diff
    @@ -100,9 +103,11 @@ def __init__(
             self._write_frequency = write_frequency
             self._turn_counter: int = 0
     
    -        # Prefetch caches: session_key → last result (consumed once per turn)
    +        # Prefetch cache: session_key → last context result (consumed once per turn).
    +        # Dialectic results are cached on the plugin side (HonchoMemoryProvider
    +        # ._prefetch_result) so session-start prewarm and turn-driven fires share
    +        # one source of truth; see __init__.py _do_session_init for the prewarm.
             self._context_cache: dict[str, dict] = {}
    -        self._dialectic_cache: dict[str, str] = {}
             self._prefetch_cache_lock = threading.Lock()
             self._dialectic_reasoning_level: str = (
                 config.dialectic_reasoning_level if config else "low"
    ```

    ```diff
    @@ -272,8 +277,10 @@ def get_or_create(self, key: str) -> HonchoSession:
                 logger.debug("Local session cache hit: %s", key)
                 return self._cache[key]
     
    -        # Use peer names from global config when available
    -        if self._config and self._config.peer_name:
    +        # Gateway sessions should use the runtime user identity when available.
    +        if self._runtime_user_peer_name:
    +            user_peer_id = self._sanitize_id(self._runtime_user_peer_name)
    +        elif self._config and self._config.peer_name:
                 user_peer_id = self._sanitize_id(self._config.peer_name)
             else:
                 # Fallback: derive from session key
    ```

    ```diff
    @@ -499,8 +506,8 @@ def dialectic_query(
             Query Honcho's dialectic endpoint about a peer.
     
             Runs an LLM on Honcho's backend against the target peer's full
    -        representation. Higher latency than context() — call async via
    -        prefetch_dialectic() to avoid blocking the response.
    +        representation. Higher latency than context() — callers run this in
    +        a background thread (see HonchoMemoryProvider) to avoid blocking.
     
             Args:
                 session_key: The session key to query against.
    ```

    ```diff
    @@ -555,42 +562,6 @@ def dialectic_query(
                 logger.warning("Honcho dialectic query failed: %s", e)
                 return ""
     
    -    def prefetch_dialectic(self, session_key: str, query: str) -> None:
    -        """
    -        Fire a dialectic_query in a background thread, caching the result.
    -
    -        Non-blocking. The result is available via pop_dialectic_result()
    -        on the next call (typically the following turn). Reasoning level
    -        is selected dynamically based on query complexity.
    -
    -        Args:
    -            session_key: The session key to query against.
    -            query: The user's current message, used as the query.
    -        """
    -        def _run():
    -            result = self.dialectic_query(session_key, query)
    -            if result:
    -                self.set_dialectic_result(session_key, result)
    -
    -        t = threading.Thread(target=_run, name="honcho-dialectic-prefetch", daemon=True)
    -        t.start()
    -
    -    def set_dialectic_result(self, session_key: str, result: str) -> None:
    -        """Store a prefetched dialectic result in a thread-safe way."""
    -        if not result:
    -            return
    -        with self._prefetch_cache_lock:
    -            self._dialectic_cache[session_key] = result
    -
    -    def pop_dialectic_result(self, session_key: str) -> str:
    -        """
    -        Return and clear the cached dialectic result for this session.
    -
    -        Returns empty string if no result is ready yet.
    -        """
    -        with self._prefetch_cache_lock:
    -            return self._dialectic_cache.pop(session_key, "")
    -
         def prefetch_context(self, session_key: str, user_message: str | None = None) -> None:
             """
             Fire get_prefetch_context in a background thread, caching the result.
    ```
  - `run_agent.py`

    ```diff
    @@ -1306,31 +1306,6 @@ def __init__(
                 try:
                     _mem_provider_name = mem_config.get("provider", "") if mem_config else ""
     
    -                # Auto-migrate: if Honcho was actively configured (enabled +
    -                # credentials) but memory.provider is not set, activate the
    -                # honcho plugin automatically.  Just having the config file
    -                # is not enough — the user may have disabled Honcho or the
    -                # file may be from a different tool.
    -                if not _mem_provider_name:
    -                    try:
    -                        from plugins.memory.honcho.client import HonchoClientConfig as _HCC
    -                        _hcfg = _HCC.from_global_config()
    -                        if _hcfg.enabled and (_hcfg.api_key or _hcfg.base_url):
    -                            _mem_provider_name = "honcho"
    -                            # Persist so this only auto-migrates once
    -                            try:
    -                                from hermes_cli.config import load_config as _lc, save_config as _sc
    -                                _cfg = _lc()
    -                                _cfg.setdefault("memory", {})["provider"] = "honcho"
    -                                _sc(_cfg)
    -                            except Exception:
    -                                pass
    -                            if not self.quiet_mode:
    -                                print("  ✓ Auto-migrated Honcho to memory provider plugin.")
    -                                print("    Your config and data are preserved.\n")
    -                    except Exception:
    -                        pass
    -
                     if _mem_provider_name:
                         from agent.memory_manager import MemoryManager as _MemoryManager
                         from plugins.memory import load_memory_provider as _load_mem
    ```
  - `scripts/release.py`

    ```diff
    @@ -215,6 +215,7 @@
         "[redacted:email]": "ziliangpeng",
         "[redacted:email]": "centripetal-star",
         "[redacted:email]": "LeonSGP43",
    +    "[redacted:email]": "LeonSGP43",
         "[redacted:email]": "Lubrsy706",
         "[redacted:email]": "spniyant",
         "[redacted:email]": "olafthiele",
    ```
  - `tests/agent/test_memory_provider.py`

    ```diff
    @@ -971,8 +971,6 @@ def test_queue_prefetch_respects_dialectic_cadence(self):
             class FakeManager:
                 def prefetch_context(self, key, query=None):
                     pass
    -            def prefetch_dialectic(self, key, query):
    -                pass
     
             p._manager = FakeManager()
     
    ```
  - `tests/agent/test_memory_user_id.py`

    ```diff
    @@ -208,34 +208,81 @@ def test_different_users_get_different_ids(self):
     
     
     class TestHonchoUserIdScoping:
    -    """Verify Honcho plugin uses gateway user_id for peer_name when provided."""
    +    """Verify Honcho plugin keeps runtime user scoping separate from config peer_name."""
     
    -    def test_gateway_user_id_overrides_peer_name(self):
    -        """When user_id is in kwargs and no explicit peer_name, user_id should be used."""
    +    def test_gateway_user_id_is_passed_as_runtime_peer(self):
    +        """Gateway user_id should scope Honcho sessions without mutating config peer_name."""
             from plugins.memory.honcho import HonchoMemoryProvider
     
             provider = HonchoMemoryProvider()
     
    -        # Create a mock config with NO explicit peer_name
             mock_cfg = MagicMock()
             mock_cfg.enabled = True
             mock_cfg.api_key = "[redacted:secret-assignment]"
             mock_cfg.base_url = None
    -        mock_cfg.peer_name = ""  # No explicit peer_name — user_id should fill it
    -        mock_cfg.recall_mode = "tools"  # Use tools mode to defer session init
    +        mock_cfg.peer_name = "static-user"
    +        mock_cfg.recall_mode = "context"
    +        mock_cfg.context_tokens = None
    +        mock_cfg.raw = {}
    +        mock_cfg.dialectic_depth = 1
    +        mock_cfg.dialectic_depth_levels = None
    +        mock_cfg.init_on_session_start = False
    +        mock_cfg.ai_peer = "hermes"
    +        mock_cfg.resolve_session_name.return_value = "test-sess"
    +        mock_cfg.session_strategy = "shared"
     
             with patch(
                 "plugins.memory.honcho.client.HonchoClientConfig.from_global_config",
                 return_value=mock_cfg,
    -        ):
    +        ), patch(
    +            "plugins.memory.honcho.client.get_honcho_client",
    +            return_value=MagicMock(),
    +        ), patch(
    +            "plugins.memory.honcho.session.HonchoSessionManager",
    +        ) as mock_manager_cls:
    +            mock_manager = MagicMock()
    +            mock_manager.get_or_create.return_value = MagicMock(messages=[])
    +            mock_manager_cls.return_value = mock_manager
                 provider.initialize(
                     session_id="test-sess",
                     user_id="discord_user_789",
                     platform="discord",
                 )
     
    -        # The config's peer_name should have been overridden with the user_id
    -        assert mock_cfg.peer_name == "discord_user_789"
    +        assert mock_cfg.peer_name == "static-user"
    +        assert mock_manager_cls.call_args.kwargs["runtime_user_peer_name"] == "discord_user_789"
    +
    +    def test_session_manager_prefers_runtime_user_id_over_config_peer_name(self):
    +        """Session manager should isolate gateway users even when config peer_name is static."""
    +        from plugins.memory.honcho.session import HonchoSessionManager
    +
    +        mock_cfg = MagicMock()
    +        mock_cfg.peer_name = "static-user"
    +        mock_cfg.ai_peer = "hermes"
    +        mock_cfg.write_frequency = "sync"
    +        mock_cfg.dialectic_reasoning_level = "low"
    +        mock_cfg.dialectic_dynamic = True
    +        mock_cfg.dialectic_max_chars = 600
    +        mock_cfg.observation_mode = "directional"
    +        mock_cfg.user_observe_me = True
    +        mock_cfg.user_observe_others = True
    +        mock_cfg.ai_observe_me = True
    +        mock_cfg.ai_observe_others = True
    +
    +        manager = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=mock_cfg,
    +            runtime_user_peer_name="discord_user_789",
    +        )
    +
    +        with patch.object(manager, "_get_or_create_peer", return_value=MagicMock()), patch.object(
    +            manager,
    +            "_get_or_create_honcho_session",
    +            return_value=(MagicMock(), []),
    +        ):
    +            session = manager.get_or_create("discord:channel-1")
    +
    +        assert session.user_peer_id == "discord_user_789"
     
         def test_no_user_id_preserves_config_peer_name(self):
             """Without user_id, the config peer_name should be preserved."""
    ```
  - `tests/honcho_plugin/test_async_memory.py`

    ```diff
    @@ -460,10 +460,3 @@ def test_set_and_pop_context_result(self):
             assert mgr.pop_context_result("cli:test") == payload
             assert mgr.pop_context_result("cli:test") == {}
     
    -    def test_set_and_pop_dialectic_result(self):
    -        mgr = _make_manager(write_frequency="turn")
    -
    -        mgr.set_dialectic_result("cli:test", "Resume with toolset cleanup")
    -
    -        assert mgr.pop_dialectic_result("cli:test") == "Resume with toolset cleanup"
    -        assert mgr.pop_dialectic_result("cli:test") == ""
    ```
  - `tests/honcho_plugin/test_cli.py`

    ```diff
    @@ -26,6 +26,9 @@ class FakeConfig:
                 write_frequency = "async"
                 session_strategy = "per-session"
                 context_tokens = 800
    +            dialectic_reasoning_level = "low"
    +            reasoning_level_cap = "high"
    +            reasoning_heuristic = True
     
                 def resolve_session_name(self):
                     return "hermes"
    ```
  - `tests/honcho_plugin/test_session.py`

    ```diff
    @@ -568,15 +568,15 @@ def _make_provider_with_config(self, recall_mode="tools", init_on_session_start=
     
             with patch("plugins.memory.honcho.client.HonchoClientConfig.from_global_config", return_value=cfg), \
                  patch("plugins.memory.honcho.client.get_honcho_client", return_value=MagicMock()), \
    -             patch("plugins.memory.honcho.session.HonchoSessionManager", return_value=mock_manager), \
    +             patch("plugins.memory.honcho.session.HonchoSessionManager", return_value=mock_manager) as mock_manager_cls, \
                  patch("hermes_constants.get_hermes_home", return_value=MagicMock()):
                 provider.initialize(session_id="test-session-001", **init_kwargs)
     
    -        return provider, cfg
    +        return provider, cfg, mock_manager_cls
     
         def test_tools_lazy_default(self):
             """tools + initOnSessionStart=false → session NOT initialized after initialize()."""
    -        provider, _ = self._make_provider_with_config(
    +        provider, _, _ = self._make_provider_with_config(
                 recall_mode="tools", init_on_session_start=False,
             )
             assert provider._session_initialized is False
    ```

    ```diff
    @@ -585,7 +585,7 @@ def test_tools_lazy_default(self):
     
         def test_tools_eager_init(self):
             """tools + initOnSessionStart=true → session IS initialized after initialize()."""
    -        provider, _ = self._make_provider_with_config(
    +        provider, _, _ = self._make_provider_with_config(
                 recall_mode="tools", init_on_session_start=True,
             )
             assert provider._session_initialized is True
    ```

    ```diff
    @@ -593,33 +593,34 @@ def test_tools_eager_init(self):
     
         def test_tools_eager_prefetch_still_empty(self):
             """tools mode with eager init still returns empty from prefetch() (no auto-injection)."""
    -        provider, _ = self._make_provider_with_config(
    +        provider, _, _ = self._make_provider_with_config(
                 recall_mode="tools", init_on_session_start=True,
             )
             assert provider.prefetch("test query") == ""
     
         def test_tools_lazy_prefetch_empty(self):
             """tools mode with lazy init also returns empty from prefetch()."""
    -        provider, _ = self._make_provider_with_config(
    +        provider, _, _ = self._make_provider_with_config(
                 recall_mode="tools", init_on_session_start=False,
             )
             assert provider.prefetch("test query") == ""
     
         def test_explicit_peer_name_not_overridden_by_user_id(self):
             """Explicit peerName in config must not be replaced by gateway user_id."""
    -        _, cfg = self._make_provider_with_config(
    +        _, cfg, _ = self._make_provider_with_config(
                 recall_mode="tools", init_on_session_start=True,
                 peer_name="Kathie", user_id="8439114563",
             )
             assert cfg.peer_name == "Kathie"
     
         def test_user_id_used_when_no_peer_name(self):
    -        """Gateway user_id is used as peer_name when no explicit peerName configured."""
    -        _, cfg = self._make_provider_with_config(
    +        """Gateway user_id is passed separately from config peer_name."""
    +        _, cfg, mock_manager_cls = self._make_provider_with_config(
                 recall_mode="tools", init_on_session_start=True,
                 peer_name=None, user_id="8439114563",
             )
    -        assert cfg.peer_name == "8439114563"
    +        assert cfg.peer_name is None
    +        assert mock_manager_cls.call_args.kwargs["runtime_user_peer_name"] == "8439114563"
     
     
     class TestPerSessionMigrateGuard:
    ```

    ```diff
    @@ -815,6 +816,27 @@ def test_long_query_truncated(self):
     # ---------------------------------------------------------------------------
     
     
    +def _settle_prewarm(provider):
    +    """Wait for the session-start prewarm dialectic thread, then return the
    +    provider to a clean 'nothing fired yet' state so cadence/first-turn/
    +    trivial-prompt tests can assert from a known baseline."""
    +    if provider._prefetch_thread:
    +        provider._prefetch_thread.join(timeout=3.0)
    +    with provider._prefetch_lock:
    +        provider._prefetch_result = ""
    +        provider._prefetch_result_fired_at = -999
    +    provider._prefetch_thread = None
    +    provider._prefetch_thread_started_at = 0.0
    +    provider._last_dialectic_turn = -999
    +    provider._dialectic_empty_streak = 0
    +    if getattr(provider, "_manager", None) is not None:
    +        try:
    +            provider._manager.dialectic_query.reset_mock()
    +            provider._manager.prefetch_context.reset_mock()
    +        except AttributeError:
    +            pass
    +
    +
     class TestDialecticCadenceDefaults:
         """Regression tests for dialectic_cadence default value."""
     
    ```

    ```diff
    @@ -840,12 +862,15 @@ def _make_provider(cfg_extra=None):
                  patch("hermes_constants.get_hermes_home", return_value=MagicMock()):
                 provider.initialize(session_id="test-session-001")
     
    +        _settle_prewarm(provider)
             return provider
     
    -    def test_default_is_3(self):
    -        """Default dialectic_cadence should be 3 to avoid per-turn LLM calls."""
    +    def test_unset_falls_back_to_1(self):
    +        """Unset dialecticCadence falls back to 1 (every turn) for backwards
    +        compatibility with existing configs that predate the setting. The
    +        setup wizard writes 2 explicitly on new configs."""
             provider = self._make_provider()
    -        assert provider._dialectic_cadence == 3
    +        assert provider._dialectic_cadence == 1
     
         def test_config_override(self):
             """dialecticCadence from config overrides the default."""
    ```

    ```diff
    @@ -908,6 +933,7 @@ def _make_provider(cfg_extra=None):
                  patch("hermes_constants.get_hermes_home", return_value=MagicMock()):
                 provider.initialize(session_id="test-session-001")
     
    +        _settle_prewarm(provider)
             return provider
     
         def test_default_depth_is_1(self):
    ```

    ```diff
    @@ -1027,60 +1053,598 @@ def test_run_dialectic_depth_two_passes(self):
             assert provider._manager.dialectic_query.call_count == 2
             assert "Synthesis" in result
     
    -    def test_first_turn_runs_dialectic_synchronously(self):
    -        """First turn should fire the dialectic synchronously (cold start)."""
    -        from unittest.mock import MagicMock, patch
    -        provider = self._make_provider(cfg_extra={"dialectic_depth": 1})
    +    def test_run_dialectic_depth_bails_early_on_strong_signal(self):
    +        """Depth 2 skips pass 1 when pass 0 returns strong signal."""
    +        from unittest.mock import MagicMock
    +        provider = self._make_provider(cfg_extra={"dialectic_depth": 2})
             provider._manager = MagicMock()
    -        provider._manager.dialectic_query.return_value = "cold start synthesis"
    -        provider._manager.get_prefetch_context.return_value = None
    -        provider._manager.pop_context_result.return_value = None
    +        provider._manager.dialectic_query.return_value = (
    +            "## Full Assessment\n- Strong structured response\n- With evidence\n" + "x" * 200
    +        )
             provider._session_key = "test"
    -        provider._base_context_cache = ""  # cold start
    -        provider._last_dialectic_turn = -999  # never fired
    +        provider._base_context_cache = "existing context"
     
    -        result = provider.prefetch("hello world")
    -        assert "cold start synthesis" in result
    +        result = provider._run_dialectic_depth("test query")
    +        # Only 1 call because pass 0 had sufficient signal
             assert provider._manager.dialectic_query.call_count == 1
    -        # After first-turn sync, _last_dialectic_turn should be updated
    -        assert provider._last_dialectic_turn != -999
     
    -    def test_first_turn_dialectic_does_not_double_fire(self):
    -        """After first-turn sync dialectic, queue_prefetch should skip (cadence)."""
    -        from unittest.mock import MagicMock
    -        provider = self._make_provider(cfg_extra={"dialectic_depth": 1})
    -        provider._manager = MagicMock()
    -        provider._manager.dialectic_query.return_value = "cold start synthesis"
    -        provider._manager.get_prefetch_context.return_value = None
    -        provider._manager.pop_context_result.return_value = None
    +
    +# ---------------------------------------------------------------------------
    +# Trivial-prompt heuristic + dialectic cadence silent-failure guards
    +# ---------------------------------------------------------------------------
    +
    +
    +class TestTrivialPromptHeuristic:
    +    """Trivial prompts ('ok', 'y', slash commands) must short-circuit injection."""
    +
    +    @staticmethod
    +    def _make_provider():
    +        from unittest.mock import patch, MagicMock
    +        from plugins.memory.honcho.client import HonchoClientConfig
    +
    +        cfg = HonchoClientConfig(api_key="[redacted:secret-assignment]", enabled=True, recall_mode="hybrid")
    +        provider = HonchoMemoryProvider()
    +        mock_manager = MagicMock()
    +        mock_session = MagicMock()
    +        mock_session.messages = []
    +        mock_manager.get_or_create.return_value = mock_session
    +
    +        with patch("plugins.memory.honcho.client.HonchoClientConfig.from_global_config", return_value=cfg), \
    +             patch("plugins.memory.honcho.client.get_honcho_client", return_value=MagicMock()), \
    +             patch("plugins.memory.honcho.session.HonchoSessionManager", return_value=mock_manager), \
    +             patch("hermes_constants.get_hermes_home", return_value=MagicMock()):
    +            provider.initialize(session_id="test-session-trivial")
    +        _settle_prewarm(provider)
    +        return provider
    +
    +    def test_classifier_catches_common_trivial_forms(self):
    +        for t in ("ok", "OK", " ok ", "y", "yes", "sure", "thanks", "lgtm", "/help", "", "   "):
    +            assert HonchoMemoryProvider._is_trivial_prompt(t), f"expected trivial: {t!r}"
    +
    +    def test_classifier_lets_substantive_prompts_through(self):
    +        for t in ("hello world", "what's my name", "explain this", "ok so what's next"):
    +            assert not HonchoMemoryProvider._is_trivial_prompt(t), f"expected non-trivial: {t!r}"
    +
    +    def test_prefetch_skips_on_trivial_prompt(self):
    +        provider = self._make_provider()
             provider._session_key = "test"
    -        provider._base_context_cache = ""
    -        provider._last_dialectic_turn = -999
    -        provider._turn_count = 0
    +        provider._base_context_cache = "cached base"
    +        provider._last_dialectic_turn = 0
    +        provider._turn_count = 5
     
    -        # First turn fires sync dialectic
    -        provider.prefetch("hello")
    -        assert provider._manager.dialectic_query.call_count == 1
    +        assert provider.prefetch("ok") == ""
    +        assert provider.prefetch("/help") == ""
    +        # Dialectic should not have fired
    +        assert provider._manager.dialectic_query.call_count == 0
     
    -        # Now queue_prefetch on same turn should skip (cadence: 0 - 0 < 3)
    +    def test_queue_prefetch_skips_on_trivial_prompt(self):
    +        provider = self._make_provider()
    +        provider._session_key = "test"
    +        provider._turn_count = 10
    +        provider._last_dialectic_turn = -999  # would otherwise fire
    +        # initialize() pre-warms; clear call counts before the assertion.
    +        provider._manager.prefetch_context.reset_mock()
             provider._manager.dialectic_query.reset_mock()
    -        provider.queue_prefetch("hello")
    +
    +        provider.queue_prefetch("y")
    +        # Trivial prompts short-circuit both context refresh and dialectic fire.
    +        assert provider._manager.prefetch_context.call_count == 0
             assert provider._manager.dialectic_query.call_count == 0
     
    -    def test_run_dialectic_depth_bails_early_on_strong_signal(self):
    -        """Depth 2 skips pass 1 when pass 0 returns strong signal."""
    -        from unittest.mock import MagicMock
    -        provider = self._make_provider(cfg_extra={"dialectic_depth": 2})
    -        provider._manager = MagicMock()
    -        provider._manager.dialectic_query.return_value = (
    -            "## Full Assessment\n- Strong structured response\n- With evidence\n" + "x" * 200
    +
    +class TestDialecticCadenceAdvancesOnSuccess:
    +    """Cadence tracker advances only when the dialectic call returns a
    +    non-empty result. Empty results (transient API error, sparse representation)
    +    must retry on the next eligible turn instead of waiting the full cadence."""
    +
    +    @staticmethod
    +    def _make_provider():
    +        from unittest.mock import patch, MagicMock
    +        from plugins.memory.honcho.client import HonchoClientConfig
    +
    +        cfg = HonchoClientConfig(
    +            api_key="[redacted:secret-assignment]", enabled=True, recall_mode="hybrid", dialectic_depth=1,
             )
    +        provider = HonchoMemoryProvider()
    +        mock_manager = MagicMock()
    +        mock_session = MagicMock()
    +        mock_session.messages = []
    +        mock_manager.get_or_create.return_value = mock_session
    +
    +        with patch("plugins.memory.honcho.client.HonchoClientConfig.from_global_config", return_value=cfg), \
    +             patch("plugins.memory.honcho.client.get_honcho_client", return_value=MagicMock()), \
    +             patch("plugins.memory.honcho.session.HonchoSessionManager", return_value=mock_manager), \
    +             patch("hermes_constants.get_hermes_home", return_value=MagicMock()):
    +            provider.initialize(session_id="test-session-retry")
    +        _settle_prewarm(provider)
    +        return provider
    +
    +    def test_empty_dialectic_result_does_not_advance_cadence(self):
    +        import time as _time
    +        provider = self._make_provider()
             provider._session_key = "test"
    -        provider._base_context_cache = "existing context"
    +        provider._manager.dialectic_query.return_value = ""  # silent failure
    +        provider._turn_count = 5
    +        provider._last_dialectic_turn = 0  # would fire (5 - 0 = 5 ≥ 3)
     
    -        result = provider._run_dialectic_depth("test query")
    -        # Only 1 call because pass 0 had sufficient signal
    +        provider.queue_prefetch("hello")
    +        # wait for the background thread to settle
    +        if provider._prefetch_thread:
    +            provider._prefetch_thread.join(timeout=2.0)
    +
    +        # Dialectic call was attempted
             assert provider._manager.dialectic_query.call_count == 1
    +        # But cadence tracker did NOT advance — next turn should retry
    +        assert provider._last_dialectic_turn == 0
    +
    +    def test_non_empty_dialectic_result_advances_cadence(self):
    +        provider = self._make_provider()
    +        provider._session_key = "test"
    +        provider._manager.dialectic_query.return_value = "real synthesis output"
    +        provider._turn_count = 5
    +        provider._last_dialectic_turn = 0
    +
    +        provider.queue_prefetch("hello")
    +        if provider._prefetch_thread:
    +            provider._prefetch_thread.join(timeout=2.0)
    +
    +        assert provider._last_dialectic_turn == 5
    +
    +    def test_in_flight_thread_is_not_stacked(self):
    +        import threading as _threading
    +        import time as _time
    +        provider = self._make_provider()
    +        provider._session_key = "test"
    +        provider._turn_count = 10
    +        provider._last_dialectic_turn = 0
    +
    +        # Simulate a prior thread still running (fresh, not stale)
    +        hold = _threading.Event()
    +
    +        def _block():
    +            hold.wait(timeout=5.0)
    +
    +        fresh = _threading.Thread(target=_block, daemon=True)
    +        fresh.start()
    +        provider._prefetch_thread = fresh
    +        provider._prefetch_thread_started_at = _time.monotonic()  # fresh start
    +
    +        provider.queue_prefetch("hello")
    +        # Should have short-circuited — no new dialectic call
    +        assert provider._manager.dialectic_query.call_count == 0
    +        hold.set()
    +        fresh.join(timeout=2.0)
    +
    +
    +class TestSessionStartDialecticPrewarm:
    +    """Session-start prewarm fires a depth-aware dialectic whose result is
    +    consumed by turn 1 — no duplicate .chat() and no dead-cache orphaning."""
    +
    +    @staticmethod
    +    def _make_provider(cfg_extra=None, dialectic_result="prewarm synthesis"):
    +        from unittest.mock import patch, MagicMock
    +        from plugins.memory.honcho.client import HonchoClientConfig
    +
    +        defaults = dict(api_key="[redacted:secret-assignment]", enabled=True, recall_mode="hybrid")
    +        if cfg_extra:
    +            defaults.update(cfg_extra)
    +        cfg = HonchoClientConfig(**defaults)
    +        provider = HonchoMemoryProvider()
    +        mock_manager = MagicMock()
    +        mock_manager.get_or_create.return_value = MagicMock(messages=[])
    +        mock_manager.get_prefetch_context.return_value = None
    +        mock_manager.pop_context_result.return_value = None
    +        mock_manager.dialectic_query.return_value = dialectic_result
    +
    +        with patch("plugins.memory.honcho.client.HonchoClientConfig.from_global_config", return_value=cfg), \
    +             patch("plugins.memory.honcho.client.get_honcho_client", return_value=MagicMock()), \
    +             patch("plugins.memory.honcho.session.HonchoSessionManager", return_value=mock_manager), \
    +             patch("hermes_constants.get_hermes_home", return_value=MagicMock()):
    +            provider.initialize(session_id="test-prewarm")
    +        return provider
    +
    +    def test_prewarm_populates_prefetch_result(self):
    +        p = self._make_provider()
    +        # Wait for prewarm thread to land
    +        if p._prefetch_thread:
    +            p._prefetch_thread.join(timeout=3.0)
    +        with p._prefetch_lock:
    +            assert p._prefetch_result == "prewarm synthesis"
    +        assert p._last_dialectic_turn == 0
    +
    +    def test_turn1_consumes_prewarm_without_duplicate_dialectic(self):
    +        """With prewarm result already in _prefetch_result, turn 1 prefetch
    +        should NOT fire another dialectic."""
    +        p = self._make_provider()
    +        if p._prefetch_thread:
    +            p._prefetch_thread.join(timeout=3.0)
    +        p._manager.dialectic_query.reset_mock()
    +        p._session_key = "test-prewarm"
    +        p._base_context_cache = ""
    +        p._turn_count = 1
    +
    +        result = p.prefetch("hello world")
    +        assert "prewarm synthesis" in result
    +        # The sync first-turn path must NOT have fired another .chat()
    +        assert p._manager.dialectic_query.call_count == 0
    +
    +    def test_turn1_falls_back_to_sync_when_prewarm_missing(self):
    +        """If the prewarm produced nothing (empty graph, API blip), turn 1
    +        still fires its own sync dialectic."""
    +        p = self._make_provider(dialectic_result="")  # prewarm returns empty
    +        if p._prefetch_thread:
    +            p._prefetch_thread.join(timeout=3.0)
    +        with p._prefetch_lock:
    +            assert p._prefetch_result == ""  # prewarm landed nothing
    +        # Switch dialectic_query to return something on the sync first-turn call
    +        p._manager.dialectic_query.return_value = "sync recovery"
    +        p._manager.dialectic_query.reset_mock()
    +        p._session_key = "test-prewarm"
    +        p._base_context_cache = ""
    +        p._turn_count = 1
    +
    +        result = p.prefetch("hello world")
    +        assert "sync recovery" in result
    +        assert p._manager.dialectic_query.call_count == 1
    +
    +
    +class TestDialecticLiveness:
    +    """Liveness + observability: stale-thread recovery, stale-result discard,
    +    empty-streak backoff, and the snapshot method used for diagnostics."""
    +
    +    @staticmethod
    +    def _make_provider(cfg_extra=None):
    +        from unittest.mock import patch, MagicMock
    +        from plugins.memory.honcho.client import HonchoClientConfig
    +
    +        defaults = dict(api_key="[redacted:secret-assignment]", enabled=True, recall_mode="hybrid", timeout=2.0)
    +        if cfg_extra:
    +            defaults.update(cfg_extra)
    +        cfg = HonchoClientConfig(**defaults)
    +        provider = HonchoMemoryProvider()
    +        mock_manager = MagicMock()
    +        mock_manager.get_or_create.return_value = MagicMock(messages=[])
    +        mock_manager.get_prefetch_context.return_value = None
    +        mock_manager.pop_context_result.return_value = None
    +        mock_manager.dialectic_query.return_value = ""  # default: silent
    +
    +        with patch("plugins.memory.honcho.client.HonchoClientConfig.from_global_config", return_value=cfg), \
    +             patch("plugins.memory.honcho.client.get_honcho_client", return_value=MagicMock()), \
    +             patch("plugins.memory.honcho.session.HonchoSessionManager", return_value=mock_manager), \
    +             patch("hermes_constants.get_hermes_home", return_value=MagicMock()):
    +            provider.initialize(session_id="test-liveness")
    +        _settle_prewarm(provider)
    +        return provider
    +
    +    def test_stale_thread_is_treated_as_dead(self):
    +        """A thread older than timeout × multiplier no longer blocks new fires."""
    +        import threading as _threading
    +        p = self._make_provider()
    +        p._session_key = "test"
    +        p._turn_count = 10
    +        p._last_dialectic_turn = 0
    +        p._manager.dialectic_query.return_value = "fresh synthesis"
    +
    +        # Plant an alive thread with an old timestamp (stale)
    +        hold = _threading.Event()
    +        stuck = _threading.Thread(target=lambda: hold.wait(timeout=10.0), daemon=True)
    +        stuck.start()
    +        p._prefetch_thread = stuck
    +        # timeout=2.0, multiplier=2.0, so anything older than 4s is stale
    +        p._prefetch_thread_started_at = 0.0  # very old (1970 monotonic baseline)
    +
    +        p.queue_prefetch("hello")
    +        # New thread should have been spawned since stuck one is stale
    +        assert p._prefetch_thread is not stuck, "stale thread must be recycled"
    +        if p._prefetch_thread:
    +            p._prefetch_thread.join(timeout=2.0)
    +        assert p._manager.dialectic_query.call_count == 1
    +        hold.set()
    +        stuck.join(timeout=2.0)
    +
    +    def test_stale_pending_result_is_discarded_on_read(self):
    +        """A pending dialectic result from many turns ago is discarded
    +        instead of injected against a fresh conversational pivot."""
    +        p = self._make_provider(cfg_extra={"raw": {"dialecticCadence": 2}})
    +        p._session_key = "test"
    +        p._base_context_cache = "base ctx"
    +        with p._prefetch_lock:
    +            p._prefetch_result = "ancient synthesis"
    +            p._prefetch_result_fired_at = 1
    +        # cadence=2, multiplier=2 → stale after 4 turns since fire
    +        p._turn_count = 10
    +        p._last_dialectic_turn = 1  # prevents sync first-turn path
    +
    +        result = p.prefetch("what's new")
    +        assert "ancient synthesis" not in result, "stale pending must be discarded"
    +        # Cache slot cleared
    +        with p._prefetch_lock:
    +            assert p._prefetch_result == ""
    +            assert p._prefetch_result_fired_at == -999
    +
    +    def test_fresh_pending_result_is_kept(self):
    +        """A pending result within the staleness window is injected normally."""
    +        p = self._make_provider(cfg_extra={"raw": {"dialecticCadence": 3}})
    +        p._session_key = "test"
    +        p._base_context_cache = ""
    +        with p._prefetch_lock:
    +            p._prefetch_result = "recent synthesis"
    +            p._prefetch_result_fired_at = 8
    +        p._turn_count = 9  # 1 turn since fire, well within cadence × 2 = 6
    +        p._last_dialectic_turn = 8
    +
    +        result = p.prefetch("what's new")
    +        assert "recent synthesis" in result
    +
    +    def test_empty_streak_widens_effective_cadence(self):
    +        """After N empty returns, the gate waits cadence + N turns."""
    +        p = self._make_provider(cfg_extra={"raw": {"dialecticCadence": 1}})
    +        p._dialectic_empty_streak = 3
    +        # cadence=1, streak=3 → effective = 4
    +        assert p._effective_cadence() == 4
    +
    +    def test_backoff_is_capped(self):
    +        """Effective cadence is capped at cadence × _BACKOFF_MAX."""
    +        p = self._make_provider(cfg_extra={"raw": {"dialecticCadence": 2}})
    +        p._dialectic_empty_streak = 100
    +        # cadence=2, ceiling = 2 × 8 = 16
    +        assert p._effective_cadence() == 16
    +
    +    def test_success_resets_empty_streak(self):
    +        """A non-empty result zeroes the streak so healthy operation restores
    +        the base cadence immediately."""
    +        p = self._make_provider(cfg_extra={"raw": {"dialecticCadence": 1}})
    +        p._session_key = "test"
    +        p._dialectic_empty_streak = 5
    +        p._turn_count = 10
    +        p._last_dialectic_turn = 0
    +        p._manager.dialectic_query.return_value = "real output"
    +
    +        p.queue_prefetch("hello")
    +        if p._prefetch_thread:
    +            p._prefetch_thread.join(timeout=2.0)
    +        assert p._dialectic_empty_streak == 0
    +        assert p._last_dialectic_turn == 10
    +
    +    def test_empty_result_increments_streak(self):
    +        p = self._make_provider(cfg_extra={"raw": {"dialecticCadence": 1}})
    +        p._session_key = "test"
    +        p._turn_count = 5
    +        p._last_dialectic_turn = 0
    +        p._manager.dialectic_query.return_value = ""  # empty
    +
    +        p.queue_prefetch("hello")
    +        if p._prefetch_thread:
    +            p._prefetch_thread.join(timeout=2.0)
    +        assert p._dialectic_empty_streak == 1
    +        assert p._last_dialectic_turn == 0  # cadence not advanced
    +
    +    def test_liveness_snapshot_shape(self):
    +        p = self._make_provider()
    +        snap = p.liveness_snapshot()
    +        for key in (
    +            "turn_count", "last_dialectic_turn", "pending_result_fired_at",
    +            "empty_streak", "effective_cadence", "thread_alive", "thread_age_seconds",
    +        ):
    +            assert key in snap
    +
    +
    +class TestDialecticLifecycleSmoke:
    +    """End-to-end smoke walking a multi-turn session through prewarm,
    +    turn 1 consume, trivial skip, cadence fire, empty-result retry,
    +    heuristic bump, and session-end flush."""
    +
    +    @staticmethod
    +    def _make_provider(cfg_extra=None):
    +        from unittest.mock import patch, MagicMock
    +        from plugins.memory.honcho.client import HonchoClientConfig
    +
    +        defaults = dict(
    +            api_key="[redacted:secret-assignment]", enabled=True, recall_mode="hybrid",
    +            dialectic_reasoning_level="low", reasoning_heuristic=True,
    +            reasoning_level_cap="high", dialectic_depth=1,
    +        )
    +        if cfg_extra:
    +            defaults.update(cfg_extra)
    +        cfg = HonchoClientConfig(**defaults)
    +        provider = HonchoMemoryProvider()
    +        mock_manager = MagicMock()
    +        mock_session = MagicMock()
    +        mock_session.messages = []
    +        mock_manager.get_or_create.return_value = mock_session
    +        mock_manager.get_prefetch_context.return_value = None
    +        mock_manager.pop_context_result.return_value = None
    +
    +        with patch("plugins.memory.honcho.client.HonchoClientConfig.from_global_config", return_value=cfg), \
    +             patch("plugins.memory.honcho.client.get_honcho_client", return_value=MagicMock()), \
    +             patch("plugins.memory.honcho.session.HonchoSessionManager", return_value=mock_manager), \
    +             patch("hermes_constants.get_hermes_home", return_value=MagicMock()):
    +            return provider, mock_manager, cfg
    +
    +    def _await_thread(self, provider):
    +        if provider._prefetch_thread:
    +            provider._prefetch_thread.join(timeout=3.0)
    +
    +    def test_full_multi_turn_session(self):
    +        """Walks init → turns 1..8 → session end. Asserts at every step that
    +        the plugin did exactly what it should and nothing more.
    +
    +        Uses dialecticCadence=3 so we can exercise skip-turns between fires
    +        and the silent-failure retry path without their gates tripping each
    +        other. Trivial + slash skips apply independent of cadence.
    +        """
    +        from unittest.mock import patch, MagicMock
    +        provider, mgr, cfg = self._make_provider(
    +            cfg_extra={"raw": {"dialecticCadence": 3}}
    +        )
    +
    +        # Program the dialectic responses in the exact order they'll be requested.
    +        # An extra or missing call fails the test — strong smoke signal.
    +        responses = iter([
    +            "prewarm: user is eri, works on hermes",      # session-start prewarm
    +            "cadence fire: long query synthesis",         # turn 4 queue_prefetch
    +            "",                                           # turn 7 fire: silent failure
    +            "retry success: fresh synthesis",             # turn 8 queue_prefetch retry
    +        ])
    +        mgr.dialectic_query.side_effect = lambda *a, **kw: next(responses)
    +
    +        # ---- init: prewarm fires ----
    +        with patch("plugins.memory.honcho.client.HonchoClientConfig.from_global_config", return_value=cfg), \
    +             patch("plugins.memory.honcho.client.get_honcho_client", return_value=MagicMock()), \
    +             patch("plugins.memory.honcho.session.HonchoSessionManager", return_value=mgr), \
    +             patch("hermes_constants.get_hermes_home", return_value=MagicMock()):
    +            provider.initialize(session_id="smoke-test")
    +
    +        self._await_thread(provider)
    +        with provider._prefetch_lock:
    +            assert provider._prefetch_result.startswith("prewarm"), \
    +                "session-start prewarm must land in _prefetch_result"
    +        assert provider._last_dialectic_turn == 0, "prewarm marks turn 0"
    +        assert mgr.dialectic_query.call_count == 1
    +
    +        # ---- turn 1: consume prewarm, no duplicate dialectic ----
    +        provider.on_turn_start(1, "hey")
    +        inject1 = provider.prefetch("hey")
    +        assert "prewarm" in inject1, "turn 1 must surface prewarm"
    +        provider.sync_turn("hey", "hi there")
    +        provider.queue_prefetch("hey")  # cadence gate: (1-0)<3 → skip
    +        self._await_thread(provider)
    +        assert mgr.dialectic_query.call_count == 1, \
    +            "turn 1 must not fire — prewarm covered it and cadence skips"
    +
    +        # ---- turn 2: trivial 'ok' → skip everything ----
    +        mgr.prefetch_context.reset_mock()
    +        provider.on_turn_start(2, "ok")
    +        assert provider.prefetch("ok") == "", "trivial prompt must short-circuit injection"
    +        provider.sync_turn("ok", "cool")
    +        provider.queue_prefetch("ok")
    +        self._await_thread(provider)
    +        assert mgr.dialectic_query.call_count == 1, "trivial must not fire dialectic"
    +        assert mgr.prefetch_context.call_count == 0, "trivial must not fire context refresh"
    +
    +        # ---- turn 3: slash '/help' → also skip ----
    +        provider.on_turn_start(3, "/help")
    +        assert provider.prefetch("/help") == ""
    +        provider.queue_prefetch("/help")
    +        assert mgr.dialectic_query.call_count == 1
    +
    +        # ---- turn 4: long query → cadence fires + heuristic bumps ----
    +        long_q = "walk me through " + ("x " * 100)  # ~200 chars → heuristic +1
    +        provider.on_turn_start(4, long_q)
    +        provider.prefetch(long_q)
    +        provider.sync_turn(long_q, "sure")
    +        provider.queue_prefetch(long_q)  # (4-0)≥3 → fires
    +        self._await_thread(provider)
    +        assert mgr.dialectic_query.call_count == 2, "turn 4 cadence fire"
    +        _, kwargs = mgr.dialectic_query.call_args
    +        assert kwargs.get("reasoning_level") in ("medium", "high"), \
    +            f"long query must bump reasoning level above 'low'; got {kwargs.get('reasoning_level')}"
    +        assert provider._last_dialectic_turn == 4, "cadence tracker advances on success"
    +
    +        # ---- turns 5–6: cadence cooldown, no fires ----
    +        for t in (5, 6):
    +            provider.on_turn_start(t, "tell me more")
    +            provider.queue_prefetch("tell me more")
    +            self._await_thread(provider)
    +        assert mgr.dialectic_query.call_count == 2, "turns 5–6 blocked by cadence window"
    +
    +        # ---- turn 7: fires but silent failure (empty dialectic) ----
    +        provider.on_turn_start(7, "and then what")
    +        provider.queue_prefetch("and then what")  # (7-4)≥3 → fires
    +        self._await_thread(provider)
    +        assert mgr.dialectic_query.call_count == 3, "turn 7 fires"
    +        assert provider._last_dialectic_turn == 4, \
    +            "silent failure must NOT burn the cadence window"
    +
    +        # ---- turn 8: retries because cadence didn't advance ----
    +        provider.on_turn_start(8, "try again")
    +        provider.queue_prefetch("try again")  # (8-4)≥3 → fires again
    +        self._await_thread(provider)
    +        assert mgr.dialectic_query.call_count == 4, \
    +            "turn 8 retries because turn 7's empty result didn't advance cadence"
    +        assert provider._last_dialectic_turn == 8, "retry success advances"
    +
    +        # ---- session end: flush messages ----
    +        provider.on_session_end([])
    +        mgr.flush_all.assert_called()
    +
    +
    +class TestReasoningHeuristic:
    +    """Char-count heuristic that scales the auto-injected reasoning level by
    +    query length, clamped at reasoning_level_cap."""
    +
    +    @staticmethod
    +    def _make_provider(cfg_extra=None):
    +        from unittest.mock import patch, MagicMock
    +        from plugins.memory.honcho.client import HonchoClientConfig
    +
    +        defaults = dict(
    +            api_key="[redacted:secret-assignment]", enabled=True, recall_mode="hybrid",
    +            dialectic_reasoning_level="low", reasoning_heuristic=True,
    +            reasoning_level_cap="high",
    +        )
    +        if cfg_extra:
    +            defaults.update(cfg_extra)
    +        cfg = HonchoClientConfig(**defaults)
    +        provider = HonchoMemoryProvider()
    +        mock_manager = MagicMock()
    +        mock_manager.get_or_create.return_value = MagicMock(messages=[])
    +        with patch("plugins.memory.honcho.client.HonchoClientConfig.from_global_config", return_value=cfg), \
    +             patch("plugins.memory.honcho.client.get_honcho_client", return_value=MagicMock()), \
    +             patch("plugins.memory.honcho.session.HonchoSessionManager", return_value=mock_manager), \
    +             patch("hermes_constants.get_hermes_home", return_value=MagicMock()):
    +            provider.initialize(session_id="test-heuristic")
    +        _settle_prewarm(provider)
    +        return provider
    +
    +    def test_short_query_stays_at_base(self):
    +        p = self._make_provider()
    +        assert p._apply_reasoning_heuristic("low", "hey") == "low"
    +
    +    def test_medium_query_bumps_one_level(self):
    +        p = self._make_provider()
    +        q = "x" * 150
    +        assert p._apply_reasoning_heuristic("low", q) == "medium"
    +
    +    def test_long_query_bumps_two_levels(self):
    +        p = self._make_provider()
    +        q = "x" * 500
    +        assert p._apply_reasoning_heuristic("low", q) == "high"
    +
    +    def test_bump_respects_cap(self):
    +        p = self._make_provider(cfg_extra={"reasoning_level_cap": "medium"})
    +        q = "x" * 500  # would hit 'high' without the cap
    +        assert p._apply_reasoning_heuristic("low", q) == "medium"
    +
    +    def test_max_never_auto_selected_with_default_cap(self):
    +        p = self._make_provider(cfg_extra={"dialectic_reasoning_level": "high"})
    +        q = "x" * 500  # base=high, bump would push to 'max'
    +        assert p._apply_reasoning_heuristic("high", q) == "high"
    +
    +    def test_heuristic_disabled_returns_base(self):
    +        p = self._make_provider(cfg_extra={"reasoning_heuristic": False})
    +        q = "x" * 500
    +        assert p._apply_reasoning_heuristic("low", q) == "low"
    +
    +    def test_resolve_pass_level_applies_heuristic_at_base_mapping(self):
    +        """Depth=1, pass 0 maps to 'base' → heuristic applies."""
    +        p = self._make_provider()
    +        q = "x" * 150
    +        assert p._resolve_pass_level(0, query=q) == "medium"
    +
    +    def test_resolve_pass_level_does_not_touch_explicit_per_pass(self):
    +        """dialecticDepthLevels wins absolutely — no heuristic scaling."""
    +        p = self._make_provider(cfg_extra={"dialectic_depth_levels": ["minimal"]})
    +        q = "x" * 500  # heuristic would otherwise bump to 'high'
    +        assert p._resolve_pass_level(0, query=q) == "minimal"
    +
    +    def test_resolve_pass_level_does_not_touch_lighter_passes(self):
    +        """Depth 3 pass 0 is hardcoded 'minimal' — heuristic must not bump it."""
    +        p = self._make_provider(cfg_extra={"dialectic_depth": 3})
    +        q = "x" * 500
    +        assert p._resolve_pass_level(0, query=q) == "minimal"
    +        # But the 'base' pass (idx 1 for depth 3) does get heuristic
    +        assert p._resolve_pass_level(1, query=q) == "high"
     
     
     # ---------------------------------------------------------------------------
    ```
  - `tests/run_agent/test_memory_provider_init.py`

    ```diff
    @@ -0,0 +1,39 @@
    +"""Regression tests for memory provider selection during AIAgent init."""
    +
    +from types import SimpleNamespace
    +from unittest.mock import patch
    +
    +
    +def test_blank_memory_provider_does_not_auto_enable_honcho():
    +    """Blank memory.provider should remain opt-out even if Honcho fallback looks configured."""
    +    cfg = {"memory": {"provider": ""}, "agent": {}}
    +    honcho_cfg = SimpleNamespace(enabled=True, api_key="[redacted:secret-assignment]", base_url=None)
    +
    +    with (
    +        patch("hermes_cli.config.load_config", return_value=cfg),
    +        patch("hermes_cli.config.save_config") as save_config,
    +        patch(
    +            "plugins.memory.honcho.client.HonchoClientConfig.from_global_config",
    +            return_value=honcho_cfg,
    +        ) as from_global_config,
    +        patch("plugins.memory.load_memory_provider") as load_memory_provider,
    +        patch("agent.model_metadata.get_model_context_length", return_value=204_800),
    +        patch("run_agent.get_tool_definitions", return_value=[]),
    +        patch("run_agent.check_toolset_requirements", return_value={}),
    +        patch("run_agent.OpenAI"),
    +    ):
    +        from run_agent import AIAgent
    +
    +        agent = AIAgent(
    +            api_key="[redacted:secret-assignment]",
    +            base_url="https://openrouter.ai/api/v1",
    +            quiet_mode=True,
    +            skip_context_files=True,
    +            skip_memory=False,
    +        )
    +
    +    assert agent._memory_manager is None
    +    from_global_config.assert_not_called()
    +    load_memory_provider.assert_not_called()
    +    save_config.assert_not_called()
    +
    ```
  - `website/docs/user-guide/features/honcho.md`

    ```diff
    @@ -77,7 +77,7 @@ Cost and depth are controlled by three independent knobs:
     | Knob | Controls | Default |
     |------|----------|---------|
     | `contextCadence` | Turns between `context()` API calls (base layer refresh) | `1` |
    -| `dialecticCadence` | Turns between `peer.chat()` LLM calls (dialectic layer refresh) | `3` |
    +| `dialecticCadence` | Turns between `peer.chat()` LLM calls (dialectic layer refresh) | `2` (recommended 1–5) |
     | `dialecticDepth` | Number of `.chat()` passes per dialectic invocation (1–3) | `1` |
     
     These are orthogonal — you can have frequent context refreshes with infrequent dialectic, or deep multi-pass dialectic at low frequency. Example: `contextCadence: 1, dialecticCadence: 5, dialecticDepth: 2` refreshes base context every turn, runs dialectic every 5 turns, and each dialectic run makes 2 passes.
    ```

    ```diff
    @@ -94,6 +94,14 @@ Each pass uses a proportional reasoning level (lighter early passes, base level
     
     Passes bail out early if the prior pass returned strong signal (long, structured output), so depth 3 doesn't always mean 3 LLM calls.
     
    +### Session-Start Prewarm
    +
    +On session init, Honcho fires a dialectic call in the background at the full configured `dialecticDepth` and hands the result directly to turn 1's context assembly. A single-pass prewarm on a cold peer often returns thin output — multi-pass depth runs the audit/reconcile cycle before the user ever speaks. If prewarm hasn't landed by turn 1, turn 1 falls back to a synchronous call with a bounded timeout.
    +
    +### Query-Adaptive Reasoning Level
    +
    +The auto-injected dialectic scales `dialecticReasoningLevel` by query length: +1 level at ≥120 chars, +2 at ≥400, clamped at `reasoningLevelCap` (default `"high"`). Disable with `reasoningHeuristic: false` to pin every auto call to `dialecticReasoningLevel`. Available levels: `minimal`, `low`, `medium`, `high`, `max`.
    +
     ## Configuration Options
     
     Honcho is configured in `~/.honcho/config.json` (global) or `$HERMES_HOME/honcho.json` (profile-local). The setup wizard handles this for you.
    ```

    ```diff
    @@ -104,7 +112,7 @@ Honcho is configured in `~/.honcho/config.json` (global) or `$HERMES_HOME/honcho
     |-----|---------|-------------|
     | `contextTokens` | `null` (uncapped) | Token budget for auto-injected context per turn. Set to an integer (e.g. 1200) to cap. Truncates at word boundaries |
     | `contextCadence` | `1` | Minimum turns between `context()` API calls (base layer refresh) |
    -| `dialecticCadence` | `3` | Minimum turns between `peer.chat()` LLM calls (dialectic layer). In `tools` mode, irrelevant — model calls explicitly |
    +| `dialecticCadence` | `2` | Minimum turns between `peer.chat()` LLM calls (dialectic layer). Recommended 1–5. In `tools` mode, irrelevant — model calls explicitly |
     | `dialecticDepth` | `1` | Number of `.chat()` passes per dialectic invocation. Clamped to 1–3 |
     | `dialecticDepthLevels` | `null` | Optional array of reasoning levels per pass, e.g. `["minimal", "low", "medium"]`. Overrides proportional defaults |
     | `dialecticReasoningLevel` | `'low'` | Base reasoning level: `minimal`, `low`, `medium`, `high`, `max` |
    ```

    ```diff
    @@ -142,6 +150,41 @@ Honcho is configured in `~/.honcho/config.json` (global) or `$HERMES_HOME/honcho
     
     In `tools` mode, the model is fully in control — it calls `honcho_reasoning` when it wants, at whatever `reasoning_level` it picks. Cadence and budget settings only apply to modes with auto-injection (`hybrid` and `context`).
     
    +## Observation (Directional vs. Unified)
    +
    +Honcho models a conversation as peers exchanging messages. Each peer has two observation toggles that map 1:1 to Honcho's `SessionPeerConfig`:
    +
    +| Toggle | Effect |
    +|--------|--------|
    +| `observeMe` | Honcho builds a representation of this peer from its own messages |
    +| `observeOthers` | This peer observes the other peer's messages (feeds cross-peer reasoning) |
    +
    +Two peers × two toggles = four flags. `observationMode` is a shorthand preset:
    +
    +| Preset | User flags | AI flags | Semantics |
    +|--------|-----------|----------|-----------|
    +| `"directional"` (default) | me: on, others: on | me: on, others: on | Full mutual observation. Enables cross-peer dialectic — "what does the AI know about the user, based on what the user said and the AI replied." |
    +| `"unified"` | me: on, others: off | me: off, others: on | Shared-pool semantics — the AI observes the user's messages only, the user peer only self-models. Single-observer pool. |
    +
    +Override the preset with an explicit `observation` block for per-peer control:
    +
    +```json
    +"observation": {
    +  "user": { "observeMe": true,  "observeOthers": true },
    +  "ai":   { "observeMe": true,  "observeOthers": false }
    +}
    +```
    +
    +Common patterns:
    +
    +| Intent | Config |
    +|--------|--------|
    +| Full observation (most users) | `"observationMode": "directional"` |
    +| AI shouldn't re-model the user from its own replies | `"ai": {"observeMe": true, "observeOthers": false}` |
    +| Strong persona the AI peer shouldn't update from self-observation | `"ai": {"observeMe": false, "observeOthers": true}` |
    +
    +Server-side toggles set via the [Honcho dashboard](https://app.honcho.dev) win over local defaults — Hermes syncs them back at session init.
    +
     ## Tools
     
     When Honcho is active as the memory provider, five tools become available:
    ```
  - `website/docs/user-guide/features/memory-providers.md`

    ```diff
    @@ -82,7 +82,7 @@ hermes memory setup        # select "honcho"
     | `workspace` | host key | Shared workspace ID |
     | `contextTokens` | `null` (uncapped) | Token budget for auto-injected context per turn. Truncates at word boundaries |
     | `contextCadence` | `1` | Minimum turns between `context()` API calls (base layer refresh) |
    -| `dialecticCadence` | `3` | Minimum turns between `peer.chat()` LLM calls. Only applies to `hybrid`/`context` modes |
    +| `dialecticCadence` | `2` | Minimum turns between `peer.chat()` LLM calls. Recommended 1–5. Only applies to `hybrid`/`context` modes |
     | `dialecticDepth` | `1` | Number of `.chat()` passes per dialectic invocation. Clamped 1–3. Pass 0: cold/warm prompt, pass 1: self-audit, pass 2: reconciliation |
     | `dialecticDepthLevels` | `null` | Optional array of reasoning levels per pass, e.g. `["minimal", "low", "medium"]`. Overrides proportional defaults |
     | `dialecticReasoningLevel` | `'low'` | Base reasoning level: `minimal`, `low`, `medium`, `high`, `max` |
    ```

    ```diff
    @@ -140,23 +140,64 @@ hermes memory setup        # select "honcho"
     If you previously used `hermes honcho setup`, your config and all server-side data are intact. Just re-enable through the setup wizard again or manually set `memory.provider: honcho` to reactivate via the new system.
     :::
     
    -**Multi-agent / Profiles:**
    +**Multi-peer setup:**
     
    -Each Hermes profile gets its own Honcho AI peer while sharing the same workspace -- all profiles see the same user representation, but each agent builds its own identity and observations.
    +Honcho models conversations as peers exchanging messages — one user peer plus one AI peer per Hermes profile, all sharing a workspace. The workspace is the shared environment: the user peer is global across profiles, each AI peer is its own identity. Every AI peer builds an independent representation / card from its own observations, so a `coder` profile stays code-oriented while a `writer` profile stays editorial against the same user.
    +
    +The mapping:
    +
    +| Concept | What it is |
    +|---------|-----------|
    +| **Workspace** | Shared environment. All Hermes profiles under one workspace see the same user identity. |
    +| **User peer** (`peerName`) | The human. Shared across profiles in the workspace. |
    +| **AI peer** (`aiPeer`) | One per Hermes profile. Host key `hermes` → default; `hermes.<profile>` for others. |
    +| **Observation** | Per-peer toggles controlling what Honcho models from whose messages. `directional` (default, all four on) or `unified` (single-observer pool). |
    +
    +### New profile, fresh Honcho peer
     
     ```bash
    -hermes profile create coder --clone   # creates honcho peer "coder", inherits config from default
    +hermes profile create coder --clone
     ```
     
    -What `--clone` does: creates a `hermes.coder` host block in `honcho.json` with `aiPeer: "coder"`, shared `workspace`, inherited `peerName`, `recallMode`, `writeFrequency`, `observation`, etc. The peer is eagerly created in Honcho so it exists before first message.
    +`--clone` creates a `hermes.coder` host block in `honcho.json` with `aiPeer: "coder"`, shared `workspace`, inherited `peerName`, `recallMode`, `writeFrequency`, `observation`, etc. The AI peer is eagerly created in Honcho so it exists before the first message.
     
    -For profiles created before Honcho was set up:
    +### Existing profiles, backfill Honcho peers
     
     ```bash
    -hermes honcho sync   # scans all profiles, creates host blocks for any missing ones
    +hermes honcho sync
    +```
    +
    +Scans every Hermes profile, creates host blocks for any profile without one, inherits settings from the default `hermes` block, and creates the new AI peers eagerly. Idempotent — skips profiles that already have a host block.
    +
    +### Per-profile observation
    +
    +Each host block can override the observation config independently. Example: a code-focused profile where the AI peer observes the user but doesn't self-model:
    +
    +```json
    +"hermes.coder": {
    +  "aiPeer": "coder",
    +  "observation": {
    +    "user": { "observeMe": true, "observeOthers": true },
    +    "ai":   { "observeMe": false, "observeOthers": true }
    +  }
    +}
     ```
     
    -This inherits settings from the default `hermes` host block and creates new AI peers for each profile. Idempotent -- skips profiles that already have a host block.
    +**Observation toggles (one set per peer):**
    +
    +| Toggle | Effect |
    +|--------|--------|
    +| `observeMe` | Honcho builds a representation of this peer from its own messages |
    +| `observeOthers` | This peer observes the other peer's messages (feeds cross-peer reasoning) |
    +
    +Presets via `observationMode`:
    +
    +- **`"directional"`** (default) — all four flags on. Full mutual observation; enables cross-peer dialectic.
    +- **`"unified"`** — user `observeMe: true`, AI `observeOthers: true`, rest false. Single-observer pool; AI models the user but not itself, user peer only self-models.
    +
    +Server-side toggles set via the [Honcho dashboard](https://app.honcho.dev) win over local defaults — synced back at session init.
    +
    +See the [Honcho page](./honcho.md#observation-directional-vs-unified) for the full observation reference.
     
     <details>
     <summary>Full honcho.json example (multi-profile)</summary>
    ```

    ```diff
    @@ -181,7 +222,7 @@ This inherits settings from the default `hermes` host block and creates new AI p
           },
           "dialecticReasoningLevel": "low",
           "dialecticDynamic": true,
    -      "dialecticCadence": 3,
    +      "dialecticCadence": 2,
           "dialecticDepth": 1,
           "dialecticMaxChars": 600,
           "contextCadence": 1,
    ```
- **#11434: fix(honcho): scope gateway sessions by runtime user id**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#12419`, `#12160`
- **#12160: fix(honcho): dialectic lifecycle — defaults, retry, prewarm consumption**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#12419`, `#11434`

## Fix status

- Merged to main in PR #12419 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-19`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
