fix(honcho-memory): three-pass sanitizer for imperative, self-narration prefix, and self-narration phrase lines - #66770
Conversation
…dows _run_bash() built [bash, "-c", cmd_string] unconditionally for every local-backend command on Windows -- including when the command targets a different interpreter entirely. bash -c parses the whole string as bash source, expanding $-prefixed content inside double-quoted portions before the target program ever sees it, so `powershell -Command "...$_.Path..."` never reached PowerShell intact. terminal, write_file, and execute_code's generated hermes_tools.terminal() RPC stub all funnel through this one function, so this was one root cause surfacing in three places, not three separate bugs. Fix: detect when cmd_string is *only* a powershell/pwsh invocation (no other bash constructs mixed in, checked on tokenized output so a `|` inside a quoted PowerShell pipeline isn't mistaken for a bash pipe) and invoke the resolved interpreter directly via subprocess.Popen, skipping bash's parser entirely. Every other command is unchanged. Also wired the existing _looks_like_msys_spawn_failure()/_git_bash_aslr_help() diagnostic into write_file's failure path -- previously only the terminal tool's own _find_bash() call site used it, so the same rare MSYS crash class surfaced as a raw dump instead of the actionable remediation message when it hit during a write_file call. Verified: full existing tools/environments/local.py test suite (199 tests) identical before/after via git stash -- zero regressions.
Add _is_minimax_global_anthropic_base_url helper and extend
build_api_kwargs_extras to emit thinking:{type:adaptive} (or
disabled) on the Anthropic-compatible route at
api.minimax.io/anthropic. Previously only the OpenAI-compatible
/v1 route received the reasoning control, leaving every install
that uses the standard minimax-oauth or default minimax provider
with M3 reasoning silently off despite reasoning_effort being set.
Complements NousResearch#42560 (which handles the reasoning_config=None case
in agent/anthropic_adapter.py) — together the two cover both the
explicit-config and silent-default call patterns.
…hropic Cherry-picked from NousResearch#42560 (kapelame) with manual hunk adaptation since local main has moved past the PR's base. Three changes in agent/anthropic_adapter.py: 1. _supports_adaptive_thinking now returns True for MiniMax M3 (was False, which made the code path fall into the manual enabled+budget_tokens branch — MiniMax's /anthropic endpoint only accepts adaptive/disabled per the official docs and silently ignores manual thinking.type=enabled, producing reasoning_tokens=0). 2. New _is_minimax_m3(model) helper. 3. build_anthropic_kwargs defaults reasoning_config to {enabled: True} for M3 when caller passes None (M3 returns content=null without thinking enabled). After this, build_anthropic_kwargs emits thinking.type=adaptive for M3 on api.minimax.io/anthropic, which the endpoint actually accepts. End-to-end verified in-process: M3+None→adaptive medium, M3+enabled True effort high→adaptive high, M3+enabled False→no thinking, claude-opus-4.6 unchanged (still adaptive), M2.7 unchanged (still manual enabled+budget_tokens).
…tation The agent's own AI Self-Representation can accumulate hundreds of `hermes says X` / `hermes said X` lines from prior debugging sessions. Once surfaced, they re-assert themselves as present-tense facts in future model responses even when the underlying state has changed. This patch adds a second filter pass that demotes these self-narration lines to a labeled `[historical, demoted from <section>]` block at the end of the section, alongside the existing imperative-shape filter that demotes INSTRUCTION:/RULE:/DIRECTIVE:/COMMAND: lines. Also adds a 60-line hard cap per section. Lines past the cap go to a `[historical, truncated]` block. Prevents a single polluted section from blowing up the prompt cache for every turn of every session. Diff: extends PR NousResearch#66754 sanitizer with self-narration + line-cap passes. Backward compatible: clean sections render unchanged.
There was a problem hiding this comment.
Pull request overview
This PR adds additional sanitization and truncation logic to Honcho-rendered memory context to reduce prompt-injection/self-trust-loop pollution, and also includes unrelated fixes in local command execution and MiniMax M3 reasoning defaults.
Changes:
- Add Honcho context sanitization for imperative-shaped lines, self-narration/debug prefixes, and a per-section line cap with demoted “historical” trailers.
- Adjust local command execution to directly exec standalone PowerShell/pwsh invocations (bypassing
bash -c) to avoid$expansion/path mangling. - Fix MiniMax M3 thinking defaults on Anthropic-format routes and expand MiniMax provider route detection.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
plugins/memory/honcho/__init__.py |
Sanitizes/demotes injected or self-narrated lines and caps rendered context per section. |
tools/environments/local.py |
Adds direct-exec path for standalone PowerShell/pwsh commands to avoid Git Bash parsing issues. |
tools/file_operations.py |
Improves write-file error detail by reusing Git Bash ASLR remediation messaging on Windows/MSYS spawn failures. |
plugins/model-providers/minimax/__init__.py |
Adjusts MiniMax M3 reasoning controls for both /v1 and /anthropic routes. |
agent/anthropic_adapter.py |
Defaults MiniMax M3 thinking on (when unspecified) and treats M3 as adaptive-thinking capable. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| stripped = line.lstrip() | ||
| lower = stripped.lower() | ||
| if any(stripped.startswith(prefix) for prefix in cls._IMPERATIVE_LINE_PREFIXES): | ||
| filtered_injection.append(line) | ||
| elif any(lower.startswith(prefix) for prefix in cls._SELF_NARRATION_PREFIXES): | ||
| filtered_historical.append(line) |
| _MAX_LINES_PER_SECTION = 60 | ||
|
|
||
| @classmethod | ||
| def _sanitize_card_lines(cls, card_text: str, section_name: str) -> str: |
| rep = ctx.get("representation", "") | ||
| if rep: | ||
| parts.append(f"## User Representation\n{rep}") | ||
| sanitized_rep = self._sanitize_representation_lines(rep, "User Representation") |
| # count of what was truncated. | ||
| _MAX_LINES_PER_SECTION = 60 | ||
|
|
||
| @classmethod |
| if reasoning_config is None and _is_minimax_m3(model): | ||
| reasoning_config = {"enabled": True} |
| def _direct_interpreter_argv(cmd_string: str) -> "list[str] | None": | ||
| """Detect a command that is ONLY a direct PowerShell/pwsh invocation and | ||
| return a ready-to-exec argv list for it, or None if this command should | ||
| still go through bash -c. | ||
|
|
Related to #66754 for the Honcho context-sanitization work, but this branch also changes MiniMax transport behavior, local PowerShell execution, and file operations. Please split or explicitly review those independent changes; it is not a duplicate. |
…ervations The self-narration filter introduced in NousResearch#66770 only catches lines whose first token is 'hermes says' / 'hermes said' / '[AUTO-NARRATED]' / etc. User-peer observations of the form [2026-07-18 06:13:36] austin shared that Hermes said 'Vee'... survive the prefix filter because they start with a timestamp. But the quoted self-narration token still seeds the self-trust loop when it lands in model context — observed live with three surviving lines in the user-peer representation referencing the Vee debugging session. Add a fourth filter pass: a word-boundary-anchored regex matching 'hermes says' or 'hermes said' anywhere in the line, case-insensitive. Demotes to the same [historical, demoted from <section>] block as the prefix filter. False-positive analysis against the live user-peer corpus (12,377 chars, 125 lines): - 3 surviving-contamination lines: caught - 9 legitimate 'Hermes' mentions ('PC-Hermes-class', 'desktop hermes', 'hermes-update-now.ps1', 'quoted Hermes saying that...', 'austin says...', 'Hermes to', 'hermes-says-X'): all kept, zero false positives The word-boundary anchors prevent matching inside compound tokens like 'hermes-says-X' (hyphenated) — only free-standing 'hermes says/hermes said' phrases trigger. Tests: 9 new tests in test_self_narration_filter.py covering all four filter passes, line cap, word-boundary edge cases, case-insensitivity, and the prefix-vs-phrase interaction. All 9 pass; full Honcho test suite still green (425 passed, 5 pre-existing unrelated path-resolution failures confirmed via git stash). Stacks on PR NousResearch#66770 — third commit on fix/honcho-self-narration-demote.
|
Third commit added: Why: user-peer observations of the form Fix: word-boundary-anchored regex False-positive analysis against the live user-peer corpus (12,377 chars, 125 lines):
Tests: 9 new tests in PR title updated to reflect three-pass scope. Patch artifact at |
|
Closing and re-opening as PR #66771 from a clean branch that contains only the 2 self-narration commits. The original PR head branch had picked up MiniMax transport and PowerShell bypass commits that don't belong with the Honcho context-sanitization work — keeping them out of the new PR scope. The install-tree hash and live install behavior are unchanged; the patch is functionally identical, just isolated to the right concerns. |
Assistant output is dominated by self-narration, status reports, and tool-call traces. The Honcho deriver's extraction prompt reads assistant output as facts about the hermes peer, which inflates the AI Self-Representation with debug breadcrumbs and re-asserting "hermes said X" lines on every turn — the exact pattern that fed the 2026-07-18 self-trust loop and that PR NousResearch#66770's renderer only partially mitigates. Source-side fix is the right layer. User messages still go in (legitimate). AI identity / config / system seeds go through seed_ai_identity in the same module and are unaffected. This is the Hermes-side half of a two-part fix: - Honcho deriver prompt (exclusions) — addresses new pollution at extract time - This patch — stops new pollution at write time Both needed because: 1. Prompt exclusions only catch lines that match; some debug-style content slips through. 2. Stopping the write at source is structurally simpler and prevents the deriver from spending compute on assistant content at all. Refs PR NousResearch#66770 (renderer, defense-in-depth).
The sync_turn patch (commit bff9bc635 in this repo) only blocks assistant content from being added to the session object via the runtime chat write path. The _flush_session path that drains queued messages to Honcho is called independently from: - on_session_end hook (which calls manager.flush_all() -> _flush_session) - the async_writer_loop background daemon thread - direct flush_all() invocations from tools/ call sites Without filtering on the flush side, any assistant messages added to the session BEFORE the sync_turn patch took effect would still drain to Honcho during a gateway restart or session-end, then deriver would extract them as third-person facts about the hermes peer (hermes said X / hermes reported Y), reproducing the AI Self-Representation pollution that PR NousResearch#66770's renderer only partially mitigates on read. The 96 new pollution documents observed after the gateway restart on 2026-07-18 08:42 were from exactly this path: in-memory queue draining. Fix: filter m.get('role') == 'assistant' inside _flush_session's new_messages list comprehension, mirroring sync_turn's stance. User messages and the rare legitimate assistant_message (e.g., identity seeds via seed_ai_identity path which doesn't go through session._flush_session) are unaffected. The seed_ai_identity path was already separate and not in scope here.
fix(honcho-memory): demote self-narration lines from AI Self-Representation
Summary
Extends the PR #66754 sanitization pass in
plugins/memory/honcho/__init__.py:_sanitize_card_lineswith two new filters that address a different class of self-trust-loop pollution:Self-narration filter: lines starting with
hermes says,hermes said,HERMES SAYS:,HERMES SAID:,[AUTO-NARRATED],[DEBUG-LOG], or[SELF-TRACE]are demoted to a labeled[historical, demoted from <section>]block at the end of the section. The AI Self-Representation accumulates hundreds of these from prior debugging sessions, and once surfaced they re-assert themselves as present-tense facts in future model responses.Line cap: each section is capped at
_MAX_LINES_PER_SECTION = 60lines. Overflow goes to a labeled[historical, truncated — <section> exceeded 60-line cap; N older lines demoted]block. Prevents a single polluted section from blowing up the prompt cache for every turn.The existing imperative-shape filter from PR #66754 is preserved unchanged. Backward-compatible: clean sections render identically.
Why
Symptom observed on 2026-07-18: the Telegram bot's response to a logic puzzle included a side note that re-narrated what the actual answer already established ("Side note: the memory-context block this turn shows the sanitized User Peer Card format..."). Tracing back, the side note was triggered by the bot pattern-matching
hermes says Xobservations from its own AI Self-Representation, even though the actual response didn't need the sidebar. PR #66754's imperative-shape filter (catchINSTRUCTION:,RULE:, etc.) didn't catch this because the noise was inhermes saysform, not imperative form.The same pattern was visible across this debugging session's AI Self-Representation, which had ~70
[2026-07-18 ...] hermes says Xlines by the end. Each one became a future re-assertion target. Patching the renderer to demote these lines closes the vector at the surface layer regardless of upstream Honcho state.Repro
A session's AI Self-Representation contains dozens of
hermes says X/hermes said Ylines from prior debugging. Without the patch, the model reads them as authoritative and re-narrates them in every response, even when the response doesn't need it. With the patch, those lines are demoted to a clearly-labeled[historical, demoted]block at the end of the section so the model can see them for context but doesn't quote them as live facts.Diff scope
plugins/memory/honcho/__init__.py: extends_sanitize_card_lineswith self-narration filter, line cap, and updated trailer-block formatting. Adds_SELF_NARRATION_PREFIXEStuple and_MAX_LINES_PER_SECTIONconstant.Test coverage
In-process unit tests (verify with
python -c "..."after this PR lands):[untrusted injection]block; lowercasehermes says X→[historical, demoted]block; uppercaseHERMES SAID: Y→[historical, demoted]block;[AUTO-NARRATED] Z→[historical, demoted]block; legitimate clean lines stay in main body[historical, truncated]block with explicit count annotationhermes saysand uppercaseHERMES SAYS:both caught (matched case-insensitively because Honcho stores these in lowercase)Reviewer ask
hermes says,hermes said,HERMES SAYS:,HERMES SAID:,[AUTO-NARRATED],[DEBUG-LOG],[SELF-TRACE]) is the right set — open to additions if there are other historical/debug-tag prefixes in production Honcho dataOut of scope
honcho_conclude deleteper-row, which is a separate work item. The renderer fix is the durable prevention.Cross-references