{"slug": "refine-cycle-self-improvement-plugin-for-hermes-agent", "title": "Refine Cycle: self-improvement plugin for Hermes Agent", "summary": "Refine Cycle, a self-improvement plugin for Nous Research's Hermes Agent, automatically detects recurring errors across sessions and applies up to three reversible changes per day to memory, skills, or prompts, with all mutations journaled and credentials redacted. The plugin adapts Prime Intellect's Prime Agent /refine concept, requires a full installer that connects to the session's AI model and expands long-term memory, and includes a rollback command via `python install.py --rollback`.", "body_md": "**Your agent keeps repeating the same mistake. This makes it stop.**\n\n**Refine Cycle** looks across recent sessions, finds those repeating problems, and\nsaves one small lesson when the evidence is strong enough. Later, it checks\nwhether the same problem came back.\n\n**Cross-session by design.** Hermes can learn from the conversation in front of\nit, but some problems return across different sessions: the same failed command,\nthe same wrong assumption, the same workaround you have to explain twice.\n\nUnderneath: errors are fingerprinted into comparable shapes, recurrence is\ncounted **within and across sessions**, and every mutation is journaled before it\nruns.\n\nIt adapts the `/refine` concept from\n[Prime Intellect's Prime Agent](https://www.primeintellect.ai/blog/prime-agent)\n(Continual Harness) to the Hermes plugin system.\n\n1. **Notice what keeps going wrong.** One bad result may be noise. A problem seen\nin two sessions or five times is a pattern worth examining.\n2. **Save the smallest useful lesson.** It can add a short memory,\ncreate or improve a reusable skill, or add a focused note for future turns.\n3. **Check the result.** It watches later sessions and reports whether the lesson\nappears to be working, unused, unreliable, or too new to judge.\n\n- It makes no more than three changes per day.\n- Every change is recorded. When it can be safely undone, it gives you one command to reverse it.\n- It never rewrites Hermes's base instructions or deletes your skills.\n- API keys and other credentials are removed before conversation evidence is sent to the model.\n- If the evidence, model reply, or Hermes state is unclear, it stops instead of pretending that a lesson was applied.\n\n**Refine Cycle** does more than report problems: it can change what Hermes\nremembers. The full installer connects **Refine Cycle** to the AI model already\nserving your Hermes session and increases the space available for long-term\nmemory. When the plugin starts, it also attempts to turn off Hermes's manual\nmemory and skill approval queues so lessons do not remain pending forever.\n\nThose changes are disclosed, backed up where applicable, and reversible through\n`python install.py --rollback`. See [Installation](#installation) for the exact\nfiles, commands, and host-version checks before you run it.\n\n**Technical documentation starts here.** The sections below describe the signal\nand application gates, journal states, host patch, privacy boundaries, rollback,\nand test evidence.\n\n```\ntrajectory (state.db) → scrub → fingerprint + aggregate → signal gate\n                                                  ├→ reviewer decline → journaled no_op\n                                                  └→ proposal → guardrails + prepare\n                                                              → apply → finalized outcome\n                                                                      → usefulness ledger\n```\n\n| Stage | What happens | \n|---|---|\n| **1. Collect evidence** | Reads the last N messages of the selected session from `<HERMES_HOME>/state.db` with`mode=ro` . Credentials are redacted before downstream use. | \n| **2. Aggregate** | Normalizes errors to invariant shapes, records complete 12-character fingerprints, and counts recurrence within and across sessions. | \n| **3. Signal gate and reviewer** | Repeated patterns or explicit corrections reach the proposal model. If neither exists, a substantial session may receive one small, conservative reviewer call; a decline is a sanitized, journaled `no_op` . | \n| **4. LLM proposal** | Requests one structured `create` ,`patch` , or`no_op` proposal with an optional one-sentence, falsifiable`expected_outcome` . Kinds are`skill` ,`memory` , and`prompt` . A proposal may instead carry an`edits` array of inseparable edits under one shared reason,`expected_outcome` , and`summary` . Every model-bound field is sanitized. The proposal output budget is derived locally from the shared 15,000-character content limit and scales with`max_edits_per_proposal` ; the reviewer remains separately capped at 2,400 tokens. A cut-off, malformed, or reasoning-only reply is journaled as`llm_incomplete` rather than presented as a normal`no_op` . Skill patches receive the current complete`SKILL.md` only when it is unchanged by scrubbing and no larger than 15,000 characters. | \n| **5. Guardrails** | Enforces agent-created patch targets, fresh create names, content/frontmatter, prompt-note policy shape, size limits, daily budget, and recent-duplicate rejection. Every check runs per edit, so a later edit of a transaction is measured against the edits already applied before it. | \n| **6. Prepare** | Captures a skill's pre-edit content as both a journal snapshot and a readable `.bak` file, or memory/prompt-note recovery metadata, then appends and`fsync` s a`prepared` journal record before mutation. | \n| **7. Apply and reconcile** | Runs the standard host API for skills/memory ( `patch` maps to host`edit` ) or atomically writes the plugin-owned prompt-note store. It proves target state and records`applied` ,`pending_approval` ,`conflict` , or`error` . A`conflict` occurs when a skill patch was planned against content that changed before apply, disappeared, or can no longer be read reliably; the budget is not consumed and the edit is not advertised as reversible. Host pending approvals reconcile lazily before later runs, audit, or rollback. | \n| **8. Rollback** | Journals `rollback_prepared` before a rollback side effect. A rollback is finalized only after target-state proof; staged host rollbacks remain`pending_rollback` until approval reconciliation. | \n\nHermes ships its own background review: after a turn or a session it looks at the\ncurrent conversation and saves what is worth keeping — a useful tactic, a user\npreference, a correction. It answers **\"is there something here worth\nremembering?\"**\n\n**Refine Cycle** answers a different question, over a different window, and then\nchecks its own work:\n\n|  | Hermes background review | **Refine Cycle** | \n|---|---|---|\n| **Trigger** | anything worth keeping | proposal signal at 2 repeats; application only at 2 sessions **or** 5 occurrences | \n| **Window** | the current session | many sessions | \n| **Evidence** | the conversation as written | errors normalized to invariant shapes and fingerprinted, so `HTTP 429 for /users/8821` and`HTTP 429 for /users/9134` count as one failure | \n| **Threshold** | qualitative judgement | a cheap proposal gate followed by an application bar: distinct-session count **or** occurrence count | \n| **After the edit** | — | grades it: `working` ,`did not help` ,`unused` ,`churning` — or names honestly why no verdict exists yet (`too early` ,`no recurrence window` ,`unreliable` ) | \n| **Blast radius** | host policy | 3 edits/day, dedup window, cooldown, per-edit journal, per-edit rollback | \n\nThe two are complementary, not alternatives. Hermes captures fresh experience;\n**Refine Cycle** hunts chronic failures and measures whether its own fixes held.\n\nBoth can write to the same skills and memory, so the plugin is built to notice\nthat: a skill patch is refused outright when the target changed after planning,\nand `/refine audit` reports when an entry it created was modified by something\nelse — because an effectiveness verdict on a file someone else edited is not a\nverdict worth trusting.\n\nAn agent that fixes the same problem every week is not learning. The hard part is\nnot noticing a failure — it is knowing which failures are *chronic*, and knowing\nwhether a fix worked.\n\nThe table above says what the difference is; the part worth spelling out is why\nfingerprinting carries it. Raw error strings never repeat exactly, so volatile\nparts — ids, paths, ports, timestamps — have to collapse before \"again\" means\nanything, while genuinely different errors must stay apart. Those two\nrequirements pull against each other, and every serious defect in this plugin so\nfar has been one of them winning too hard. An edit is then treated as a\nhypothesis with a falsifiable `expected_outcome`, which is what makes a verdict\nafterwards possible at all.\n\nAmbiguous trajectories can still receive one conservative reviewer pass rather\nthan silently ending at the mechanical gate. Reviewer-approved proposals are\njournaled as advisory `reviewer_only` outcomes and are never applied without the\nnormal recurrence evidence.\n\nThe base system prompt is never touched. Only **agent-created** skills and\nmemory entries are editable; built-in, pinned, and hub-installed skills remain\noff-limits. Prompt notes live only in **Refine Cycle**'s own store, never in host\nmemory or a skill.\n\n\"The same failure happened again\" is a question about shapes, not strings.\n`HTTP 429 for /users/8821` and `HTTP 429 for /users/9134` are one failure, not\ntwo. Normalizing volatile parts and hashing the result turns a flat list of\nerror text into countable patterns.\n\nA pattern that appears in several **different** sessions is stronger evidence\nthan one repeated twice inside a conversation. Interactive prompts remain\nbounded, while `/refine audit` evaluates recurrence over the complete available\npost-edit period.\n\nThe proposal is requested via `json_schema` structured output, with an automatic\nfallback to `json_mode` and then raw-text JSON salvage for providers that reject\n`response_format.type=json_schema`.\n\nTwo arms produce the proposal. **The subagent arm is the default**: a read-only\nchild that can open skill bodies (`skills_list`/` skill_view`) before deciding,\nwhich measurably produces fewer unusable proposals than judging from\nname+description alone. It requires a bound parent turn — on hosts or in call\nforms where the subagent route is unavailable (no parent turn bound, launch\nrefused, answer unparsable), the run falls back to the **structured call**,\nwhich judges from bounded name+description overviews. The structured path is a\ndocumented fallback, not the primary route: on long sessions it does not keep\nup (in paired measurement it timed out twice out of five passes at the 4,000-row\nscan cap), so hosts whose integrations never bind a parent turn get materially\nworse proposals on long sessions. Structured proposal and reviewer calls each\nhave a 180-second timeout; the subagent wait is separately configurable via\n`proposer_subagent_timeout_seconds` (default 180).\n`proposer_subagent_strict` (default `false`) makes a subagent failure a\njournaled error (`subagent_strict_error`) instead of a downgrade.\n\nRead this before installing. A green test suite and a passing `hermes plugins doctor` do **not** prove that proposals are available. Proposal support also\nrequires `install.py --status` to recognize a compatible invocation-route patch\nand an invocation-bound smoke test to reach the proposer.\n\n| Hermes | Installs | Loads, registers | status / audit / rollback | New proposals | \n|---|---|---|---|---|\n| 0.19.0 | yes | yes | yes | yes, with the route patch | \n| 0.20.1 | yes | yes | yes | yes, with the route patch | \n| 0.20.2 | yes | yes | yes | yes, with the route patch (subagent path verified end to end) | \n| 0.21.0 | yes, after confirming a `caution` scan | yes | yes | yes, with `invocation-route-v0.21.0.patch` | \n| 0.21.1 | yes, after confirming a `caution` scan | yes | yes | yes, with the same `invocation-route-v0.21.0.patch` | \n\nUpdating Hermes rewrites its checkout, and that puts every patched file back to\nstock and takes `.refine-install` with it. Nothing warns you, and `hermes plugins doctor` still passes, because the plugin itself is untouched — only the host\ncapability it depends on is gone. New proposals then fail closed with\n`llm_invocation_unavailable` until the patch is reapplied.\n\nThis is not specific to any one release. Expect it after every Hermes update:\n\n```\npython install.py --status      # says `stock` again if the patch was removed\npython install.py --patch-only  # reapplies it\n```\n\n`--status` is also what tells you the bundled patch no longer fits a new host: it\nreports `incompatible` and names the patch bases it tried, rather than forcing a\npatch onto a topology it was not built for.\n\nMeasured on the 0.21.0 → 0.21.1 update: the checkout came back clean, the marker directory was gone, and the bundled 0.21.0 patch then applied unchanged to the new base — same eight files, its 37 host tests passing, and a proposal run afterwards reaching the session's own model in one request with no substitution.\n\nMeasured on a clean Windows checkout of Hermes 0.21.0 at\n`693641aa8b4359c602283bdbbc14041e03bc47bc`, using disposable clones and\n`HERMES_HOME` directories rather than the real profile:\n\n- the real Hermes scanner reports `caution` with 153 findings and zero critical\nfindings on the tracked plugin tree;`--force` may confirm this verdict;\n- `install.py --status` reports`stock — clean base 693641aa8b; invocation-route-v0.21.0.patch applies` ;\n- the installer transitions the disposable host from `stock` to`patched` , and\nstatus then finds all eight route markers;\n- the patch's host test file passes all 37 tests;\n- an invocation-bound synthetic proposer smoke reaches the installed proposer exactly once through the captured active client;\n- OpenAI-shaped chat, `anthropic_messages` , and`codex_responses` transports are\nroute-locked without rebuilding the active client; async calls use that same\ncaptured client and still issue one physical request;\n- rollback removes the patch-created host test and restores an empty tracked host diff;\n- the plugin suite passes 1,203 tests, with 11 Windows-only skips for Bash-based\n`install.sh` coverage.\n\nSince then the same host has been exercised with a live model rather than a\nsmoke. On a Linux checkout of the same commit, in a disposable `HERMES_HOME`\nwith its own journal, four `/refine-cycle` runs over real recorded sessions each\nreached the model and each recorded the same route facts:\n\n```\ntarget_source      : invocation_bound     ← the route came from the host, not config\nrequested / reported: openai-codex/gpt-5.6-luna-900k  (identical)\nmodel_substituted  : false\nprimary_attempts   : 1                    ← one physical request, no retry, no fallback\n```\n\nThat is the whole contract the patch exists to provide, and it holds on 0.21.0.\n\n**What those runs did not show is an applied edit.** All four ended `no_op`:\nthree because the reviewer judged the trajectory (benchmark output, exploratory\nsearches) to carry no durable lesson, one because the signal gate never opened.\nThat is the plugin declining on merit, not failing — but it means the apply path\nitself is still evidenced by the test suite and by 42 applied entries on 0.20.x,\nnot by a fresh 0.21.0 run. The apply path is downstream of the route and no\npatched file takes part in it.\n\nThe smoke uses synthetic input and does not start or restart the real gateway. The exact commands and the distinction\nbetween the original failed baseline and the corrected result are recorded in\n[`docs/FRESH-INSTALL-HERMES-0.21.0-2026-09-07.md`](/Bergschloss/Refine-Cycle-for-Hermes-Agent/blob/main/docs/FRESH-INSTALL-HERMES-0.21.0-2026-09-07.md).\n\nHermes ships its own built-in `/refine` (a background review fork), and\n`register_command` silently drops a plugin command that collides with a built-in.\nThe plugin detects this at registration and takes `/refine-cycle` instead, so\nevery subcommand stays reachable:\n\n```\n/refine-cycle status\n/refine-cycle audit\n/refine-cycle dry-run\n/refine-cycle session <session_id>\n/refine-cycle rollback <id>\n```\n\nThis matters more than a renaming usually would: typing `/refine` on such a\nhost does not fail — it reaches Hermes's own command and answers, so it is easy\nto believe you are talking to this plugin when you are not.\n\n**Do not assume this is new.** Confirmed on Hermes 0.21.0 and on 0.20.x\n(`v2026.8.31`), so treat `/refine-cycle` as the likely name and check rather than\nguess. `/refine-cycle status` names the command that answered; so does:\n\n``` python\npython -c \"import refine; print(refine._built_in_command_exists('refine'))\"\n```\n\n`True` means this plugin answers to `/refine-cycle`. Every `/refine …` example\nbelow is written for hosts without the built-in.\n\nEach bundled patch owns its marker table and target topology. Hermes 0.21.0 moved\n`gateway/run.py` to `gateway/run_inbound.py` and `run_agent.py` to\n`agent/turn_facade.py`; treating every host as the old topology would call a\ncorrectly patched host `partial`. Backup, compilation, and rollback scope are\ntherefore derived from the selected patch headers, and an existing backup cannot\nbe rebound to another topology.\n\nThe installer uses clean `git apply` only. It does **not** use `git apply -3` or\nreduce context to make a patch land: a semantic merge can compile while silently\nbreaking exact-client, one-request, or no-fallback guarantees. An unsupported\nhost fails closed with `llm_invocation_unavailable` rather than borrowing an\nambient route.\n\nDo not disable install scanning for this plugin. The tracked release tree now\nreceives a confirmable `caution` verdict rather than an unoverrideable\n`dangerous` verdict. `plugins.scan_on_install: false` remains documented only as\nan earlier diagnostic; it disables scanning for the whole profile.\n\n**Note:** this is a plugin for [Hermes Agent](https://hermes-agent.nousresearch.com/docs). It needs the plugin API available since Hermes 0.17.0 and does not run standalone. Install, registration, the full test suite, `/refine status`, and `/refine audit` are verified on Hermes 0.20.1 through 0.21.0. Only **new proposals** additionally require the matching host route patch; Hermes 0.21.0 uses `assets/invocation-route-v0.21.0.patch`. See [Hermes version support](#hermes-version-support).\n\nThe plugin lives in `<HERMES_HOME>/plugins/refine/` — `~/.hermes/plugins/refine/`\non Linux and macOS, and `%LOCALAPPDATA%\\hermes\\plugins\\refine\\` on Windows.\nUnder a Hermes profile it follows that profile; the plugin resolves the location\nthrough `hermes_constants.get_hermes_home()`.\n\n**Runtime data location.** The default `journal_dir` is\n`<HERMES_HOME>/refine`, separate from plugin source. On startup, an install\nstill using the former `<HERMES_HOME>/plugins/refine` default is migrated under\na cross-process lock: all artifacts are staged first, a completion marker is\npublished last, and the old directory is renamed rather than deleted. If any\ncopy or publication step fails, the intact legacy directory remains the active\nstore for that process and `/refine status` reports the fallback. An explicitly\nconfigured non-empty `journal_dir` is never migrated automatically.\n\nInstall the repository, run the disclosed full installer from the installed plugin directory, then enable and restart:\n\n```\nhermes plugins install Bergschloss/Refine-Cycle-for-Hermes-Agent\n# Run the next command from <HERMES_HOME>/plugins/refine:\npython install.py\nhermes plugins enable refine\nhermes gateway restart\n```\n\n`hermes plugins install` clones the repository into\n`<HERMES_HOME>/plugins/refine/`. The following `python install.py` applies and\nverifies the matching invocation-route patch and raises the two memory-limit\ntargets described below; when run from the installed directory, the plugin copy\nstep is an idempotent no-op. `plugins enable` registers the plugin, and the\nrestart activates both plugin and host changes.\n\nTo keep Hermes source untouched, omit `python install.py` or use\n`python install.py --plugin-only` from a separate checkout. The plugin can then\nprovide status, audit, rollback, and journaling, but proposal runs stop with\n`llm_invocation_unavailable`; the full two-target memory-floor change is not\nmade.\n\n**The plugin works inside the running gateway.** The LLM invocation route is\nbound by the live gateway process, so in a bare command-line process\n`refine_run` returns `llm_invocation_unavailable` by design (and `/refine status` names that blocker directly). Automatic refinement, proposals, and\napply/rollback all run inside the gateway — test with a real session or the\nrestart above, not with a one-shot script.\n\nThen, optionally, configure it in `config.yaml`:\n\n```\nplugins:\n  enabled:\n    - refine\n  entries:\n    refine:\n      journal_dir: \"<HERMES_HOME>/refine-data\"   # keep data separate from plugin source\n      llm:\n        allow_model_override: false\n        allow_provider_override: false\n```\n\n`plugins enable` manages the `enabled` list itself; the `entries` block holds\nthe plugin's own settings, and `journal_dir` keeps runtime data separate from\nplugin source (see \"Runtime data location\" above).\n\nRestart Hermes after any config change:\n\n```\nhermes gateway restart\n```\n\nVerify:\n\n```\nhermes plugins list\n# refine  1.2.0  Measurement layer ...  enabled\n```\n\nThen check that automatic refinement can actually run:\n\n```\n/refine status\n# auto: on\n# turn interval: 25\n# min messages: 15\n# cooldown: 20 min\n# edits today: 0/3\n# model: your-cheap-model @ your-provider (source: live)\n# journal: /home/you/.hermes/refine-data (does not exist yet, will be created on first write)\n# blockers: none — automatic refinement is active\n```\n\n`blockers` lists every reason a pass would not start; `warnings` lists what does\nnot stop it but will cost you later, such as runtime data sitting in the plugin\ndirectory, or a journal directory that could not be inspected at all.\n\nStatus is read-only: it creates no directory — not even the journal directory it reports on — writes no journal record, spends no budget, and calls no model. It does not reconcile pending approvals, so an unresolved staged edit still counts toward the budget it reports.\n\nThe plugin asks the LLM through Hermes's *active invocation route*: the same\nmodel binding that the user's live session uses, so that a proposal costs the\nhost's own provider creds and never a hardcoded key. Stock Hermes does not\nexpose that binding to plugins. The installer ships one patch per Hermes base:\n\n- `assets/invocation-route-v2026.8.16.patch`\n- `assets/invocation-route-v2026.8.31.patch`\n- `assets/invocation-route-v0.21.0.patch`\n\nEach patch carries its own marker table and target topology. The 0.21.0 topology\nuses `gateway/run_inbound.py` and `agent/turn_facade.py` where the older hosts\nused `gateway/run.py` and `run_agent.py`.\n\nWhich patch fits a host is decided by trying each candidate with\n`git apply --check`, not by trusting a version string. This avoids accepting a\npartially matching patch after upstream moves code while preserving hosts where\na patch still applies exactly.\n\n- **Without the patch:**`/refine status` ,`/refine audit` ,`/refine rollback` ,\njournaling, and the test suite all work. A proposal run stops honestly with`llm_invocation_unavailable` and journals the record.\n- **With the patch:** proposal runs reach the exact active route (subject to the\nconfigured trust policy).\n\nBoth installers require a clean patch and never weaken context or use a\nthree-way merge after `git apply --check` fails. `install.sh` then verifies route\nsymbols, rejects conflict markers, compiles every touched Python file, and\nimports the core module. `install.py` performs those checks and additionally runs\na synthetic invocation-bound proposer smoke in a disposable `HERMES_HOME`. If a\ncheck fails, the pre-patch state is restored. Backups are bound to the selected\npatch and topology so a later run cannot reuse them for a different host\ntransaction.\n\n```\n# from the plugin directory\n./install.sh            # apply and verify the host route patch, with backup\n```\n\n`install.sh` has no command-line mode flags; use `install.py --patch-only` when\ninstalling through the Python entry point. On hosts that already carry a complete\nknown route, the installer reports `patched` and makes no route change. Use\n`install.py --rollback` to restore the recorded pre-install state.\n\n`install.py` raises Hermes's memory character limit to a floor of **4400**. This\nis deliberate and it is for the plugin's sake, so it is stated here rather than\nleft to be discovered in a diff.\n\nStock Hermes ships `memory_char_limit: 2200` — roughly 800 tokens. That number was\nchosen when the models driving Hermes were smaller and shorter-context; a compact\nstore was the right trade then. It is no longer the constraint it was, and current\nmodels carry 4400 characters of durable memory without difficulty.\n\nFor this plugin the stock size is actively too small. Refine's whole output is lessons written into that store, and it accumulates: on a real install, six applied edits consumed about a third of the stock budget in a single day. A plugin that fills the store it depends on is not usable at 2200.\n\nTwo files are changed, because neither alone reaches everybody:\n\n- `<HERMES_HOME>/config.yaml` — Hermes writes`memory_char_limit` into the\ngenerated config, so for anyone who has already run Hermes this file is what\ndecides, and the code default is never consulted.\n- `hermes_cli/config_defaults.py` in the Hermes checkout — what a user who\ninstalls the plugin*before* Hermes has ever generated a config will get.`--plugin-only` skips this one, since that flag promises no writes into the host\ncheckout, and says so at the time.\n\nThe rule is a **floor, not an override**:\n\n| Current value | What happens | \n|---|---|\n| below 4400 (including the stock 2200) | raised to 4400 | \n| exactly 4400 | nothing | \n| above 4400 | left alone — your number wins | \n\nSo a limit you chose yourself is never overwritten and never *lowered*, and\nrunning the installer twice changes nothing the second time. `--rollback` reverses\nit by putting each file's own previous number back — not a blanket 2200, so a host\nthat installed at 3000 returns to 3000. It reverses one integer rather than\nrestoring a file copy, because `config.yaml` is a live file you edit and a restored\ncopy would silently discard everything else you changed since.\n\nThe plugin itself never hardcodes 4400. It reads whatever limit the host reports\nand shows it to you at every write (for example `memory 1443/4400`), so raising the limit\nfurther is a host decision the plugin follows rather than fights.\n\nA pass on quiet data is a `no_op` — that is the normal, correct result, not a\nfailure. The outcome families are `no_op`, `applied`, `rejected`,\n`pending_approval`, `conflict`, `llm_incomplete`, `llm_invocation_unavailable`,\nand `failed`, plus the rollback and grading terms in `/refine audit`.\n\nA real 90-day history (the long-running install this README was tested\nagainst) shows eight refine-created entries whose effectiveness verdicts\ndistribute between `too early`, `rolled back`, `rejected`, and\n`unreliable` — with `unreliable` meaning *someone else modified the artifact\nafter refine touched it*, so no verdict is possible. Expect exactly that mix:\nmost passes doing nothing, some edits reverting, and very few edits surviving\nto a `working` verdict.\n\nThe examples below use `/refine`. If the Hermes host already owns a built-in\ncommand with that name, the plugin registers as `/refine-cycle` instead; the\nregistration warning and command help show which name is active.\n\n```\n/refine\n/refine focus on Gmail API failures\n/refine audit\n/refine status\n/refine dry-run\n/refine dry-run focus on Gmail API failures\n/refine dry-run session <session_id>\n/refine session <session_id>\n/refine model\n/refine model your-cheap-model\n/refine model your-provider/your-cheap-model\n/refine model auto\n/refine rollback 1f2a3b4c5d6e\n```\n\n`audit`, `status`, `dry-run`, `model`, `session <session_id>`, and\n`rollback <12-character-id>` are exact subcommands. `status` reports whether\nautomatic refinement is active, which session and database source would be\nanalyzed, configured source skips, what blocks refinement, which model it will\nuse, and the active journal/migration state. `dry-run [reason]` runs the normal\nproposal path and journals the preview without applying an edit or consuming the\ndaily edit budget. `dry-run session <session_id>` previews one exact historical\nsession after confirming it through the read-only Hermes sessions table.\n\n`model` shows or sets the model refine asks for. Bare `model` prints the\neffective target and whether host trust allows it; `model <name>` or\n`model <provider>/<name>` pins one; `model auto` removes the override. `auto`\nreturns to the next source in the priority order, which is the configured\n`plugins.entries.refine.llm` value when there is one, and the live Hermes model\nonly when there is not. The override is stored in `model_override.json` inside\n`journal_dir` — refine does not put its own settings in the Hermes config. It\nwrites there exactly once, for one key that is not its own: see below.\n\nBoth stores are validated the same way: a provider must be a single token, a\nmodel id may be namespaced, and a value matching a credential pattern is refused\nrather than stored. A configured value that fails either rule is dropped and\nreported in `/refine status` and `/refine model`.\n\nIn the command, **the first slash is always the provider separator** and every\nlater one belongs to the model id: `/refine model openrouter/deepseek/deepseek-chat`\npins provider `openrouter` and model `deepseek/deepseek-chat`. There is therefore\nno command form for a namespaced model with no provider — set\n`plugins.entries.refine.llm.model` for that. And a pinned provider only reaches\nthe host when `allow_provider_override` is true, which `/refine model` reports.\n\nOther text is passed to the proposal model as the manual reason. That includes\ntext beginning with a subcommand word, with one deliberate exception: after\n`model`, a single token shaped like an identifier (` deepseek-v4`, `a/b`) is\ntreated as a target, so `/refine model drift` pins a model rather than asking for\na refinement about drift. Use `/refine drift` or `/refine model auto` to undo.\n\nAutomatic refinement is **enabled by default** (`auto_enabled: true`). After\nenabling the plugin and restarting Hermes, it begins analyzing sessions and\nproposing improvements without additional configuration. To disable it, set\n`auto_enabled: false` in `plugins.entries.refine`.\n\n`post_llm_call` counts the assistant messages in the history Hermes supplies and\nstarts at most one background refinement attempt once that count has grown by\n`auto_turn_interval` since this session's previous attempt. It compares a delta\nrather than an exact multiple, because a single tool-using turn appends several\nassistant messages and would otherwise step straight over the boundary. The hook\nitself does not mutate or queue work. It skips an attempt when another pass owns\nthe lock, and derives its cooldown from durable journal records, so the cooldown\nis visible across processes. `on_session_end` remains a background fallback based\non the minimum message count.\n\n```\nplugins:\n  entries:\n    refine:\n      auto_enabled: true\n      auto_min_messages: 15\n      auto_turn_interval: 25\n      auto_cooldown_minutes: 20\n```\n\nAutomatic and manual runs share a cross-thread and cross-process mutation lock, then recheck the daily budget inside that lock.\n\nWhen `min_signal_required` is enabled but the mechanical gate finds neither a\nrepeated pattern nor an explicit correction, a substantial session can receive\none structured reviewer call (`max_tokens: 2400`, timeout 180 seconds). It asks\nonly whether there is a durable lesson worth persisting. The reviewer has its\nown cooldown.\n\nA reviewer decline, malformed verdict, or reviewer error never reaches the\nproposal call. Declines are recorded as sanitized `no_op` journal entries so\nthey can be audited. An approval supplies narrow instructions to the normal\nproposal flow but remains advisory: it is journaled as `reviewer_only` and is\nnever applied without the ordinary recurrence evidence.\n\nA `prompt` proposal creates a short conditional policy in\n`<journal_dir>/prompt_notes.json`. Valid notes contain one or two policy lines\nbeginning with `When <specific condition>, <one action>.`; they are not skills,\nmemories, procedures, or system-prompt replacements.\n\n`pre_llm_call` returns a self-labelled `Refine notes:` context block. Hermes\nadds that ephemeral context to the current turn; **Refine Cycle** never reads or\nwrites the base system prompt. Injection is bounded by\n`prompt_notes_max_count` and `prompt_notes_max_chars`; when necessary it drops\nwhole oldest notes, never partial text. Empty, unavailable, unsafe, or\nout-of-scope note stores inject nothing and do not raise on the user path.\n\nInjection prefers the mutation lock but does not depend on it: the store is only ever replaced atomically, so a running refine pass never costs a turn its notes.\n\nNew prompt notes use `prompt_notes_default_scope`:\n\n- `global` (the default) is injected in every session.\n- `session` stores the session identifier resolved while reading`state.db` and\ninjects only when the hook receives that same identifier. Session notes are\nremoved from the plugin-owned store after`on_session_end` or`on_session_reset` for that session.\n\nCleanup runs on the host's callback thread, so it waits only briefly for the mutation lock instead of the full lock timeout. If a refine pass still owns the lock, the note is left in place — it can no longer be injected, because its session is gone — and it is removed at the next end or reset for that id.\n\nThat expiry is itself journaled, so a crash cannot turn \"the note landed and was\nthen cleaned up\" into \"the note never landed\": the entry moves `applied` (or\n`prepared`, for a note that landed before its own finalization completed) →\n`cleanup_prepared`, fsynced *before* the store changes, and only reaches\n`cleanup_resolved` once the exact note is proven absent from a fresh read. Both\nstates count against the daily budget, because the edit really happened — normal\nsession expiry is not a refund and not rollback evidence. Consequently a\nsession-scoped note stops being reversible once its session ends: `/refine rollback <id>` then reports the entry as not reversible, since the artifact it\nwould remove is already gone. Ledger rows for the two states read *session\ncleanup pending* and *session note expired*.\n\nCleanup removes only a note whose id, content, scope, and session still match\nthe intent recorded in the journal. A note that was hand-edited or moved to\nanother scope or session is **retained** and reported by id, and an entry already\nat `cleanup_prepared` stays there until the store is repaired. That is\ndeliberate — refine does not delete what it cannot prove it owns — but it does\nnot clear itself; see *Known integration gaps*.\n\nThe prompt-note store is plugin-owned, so there is **no host approval gate** for\nthese notes. Creation, target-state proof, audit rows, and conflict-aware\nrollback are still journaled; host approval remains in force for host-managed\nskills and memory.\n\nIf `memory.write_approval` or `skills.write_approval` is on, refine sets it to\n`false` when it registers, logs a warning naming what it changed, and leaves a\ncopy of the previous file at `config.yaml.refine-bak`.\n\nThat is a deliberate exception to \"refine does not write to the Hermes config\",\nand it exists because the gate does not do what its name suggests to an\nautonomous plugin. It queues **every** memory and skill write — the agent's own as\nmuch as refine's — and nothing lands until a human drains the queue by hand.\nNothing reports that. It presents as an agent that quietly stopped learning:\nmemory unchanged, skills missing, no error anywhere. In one real install it ran\nthat way for days, with 3 memory writes and 25 skill writes stranded and four\nskills the agent believed it had saved absent from disk.\n\nThe write is the narrowest one possible: only a `write_approval: true` line inside\nthe `memory:` or `skills:` block is rewritten, so comments, key order and every\nother value survive. The same key under any other section is left alone, and a\nconfig pinned by an administrator (managed scope) is never touched — there refine\nonly warns. `/refine status` reports the gate whenever it is on, so re-enabling it\nlater is visible rather than silent.\n\nIf you want approval gating on those subsystems, disable refine instead of turning the gate back on; the two are answers to the same question and only one of them can win.\n\nAdding a memory entry goes through the host's gated memory tool, so with\n`memory.write_approval` enabled it stages as `pending_approval` like any other\ngated write. **Removing it does not go through that gate**, and that is a\ndeliberate trade rather than an oversight.\n\nThe host's removal identifies an entry by *substring*, and pops a single match\neven when that match is a strict superstring of the text it was given. Under the\ngate a removal is staged and replayed later, so between staging and approval the\nentry can be replaced or extended — and the replay would then delete the **user's**\nentry. That is a delete of something refine never created, which this plugin may\nnever do, and it would outrank the value of the gate.\n\nSo refine removes its own append itself: it re-reads under the host's per-file memory lock, proves the entry is its own — exact content, at or after the position recorded when the edit was planned, with everything before that position pinned by a digest — and deletes only that entry, all inside the lock. If its exact text is no longer there, rollback refuses and removes nothing, and the entry stops being advertised as reversible. A longer entry that merely contains refine's text is not a problem: identification is by exact content, not substring.\n\nTwo consequences worth knowing:\n\n- A memory rollback is not reviewable through `memory.write_approval` . Skill\nrollback does stage under`skills.write_approval` — but note that staging does\nnot make it safer in this respect: the host replays a staged skill delete by\nname, without re-checking content, so a skill edited during the approval window\nis deleted as approved. Rolling back a refine-created skill while skill write\napproval is on is best done promptly, or not at all if the skill has since been\nedited by hand.\n- With the gate on and an interactive prompt registered, the *forward* memory\nwrite can block on that prompt while the refine pass holds the shared mutation\nlock, so a concurrent`/refine` waits out its lock timeout and the automatic\nsession-end pass skips that round.\n\nOne ambiguity remains and is not solvable from the host API: an entry written by something else that is byte-identical to refine's own. The host refuses exact duplicates, so this requires another writer reproducing refine's scrubbed text verbatim.\n\nSome lessons are not one edit. A new skill and the memory entry that says when to\nreach for it are inseparable: applied separately, the state between them is\ninconsistent. A proposal may therefore carry an `edits` array under one shared\nreason, `expected_outcome`, and `summary`, capped by `max_edits_per_proposal`.\n\nDurably, nothing new was invented. Each edit still gets its own journal record,\nits own recovery metadata, and its own rollback ID, tied together only by an\nadditive `group` field (`id`, `index`, `size`, `summary`, and `dropped` when\nedits were discarded). That is what keeps `/refine rollback <id>`, approval\nreconciliation, dedup, and the ledger working exactly as before — and it is why\nthe daily budget counts edits rather than proposals.\n\nEdits apply in order and the run stops at the first failure. A partial transaction is never reported as clean:\n\n- Applied and reserved edits are `applied` /`pending_approval` as usual.\n- An edit whose host write landed but whose journal finalization failed still owns a recovery ID and is listed as one.\n- Edits the daily budget refused, and edits not attempted after an earlier\nfailure, are journaled as `rejected` , which consumes no budget.\n- Edits discarded while shaping the proposal — past the cap, unusable, or\nrepeating a target already claimed in the same proposal — are counted, block a\n`completed` verdict, and are reported in`group.dropped` .\n\nSo which edits of a transaction landed is readable from the journal alone, not only from a message that automatic runs discard.\n\nThere is no `delete` action: a transaction can only create or patch.\n\n`/refine audit` reports whether refine-created entries were used and whether the\nfailure fingerprint recurred after the edit. Timestamp-aware host counts are\npreferred. If the host exposes only an all-time aggregate, the report labels it\n`all:` and does not claim post-edit use from it. Pending approvals remain marked\nas pending rather than applied. On the next audit, run, or rollback request, the\nplugin checks the host pending store and actual skill or memory target: an exact\ntarget match becomes applied, an unresolved host record stays pending, and a\nremoved host record without a target match becomes rejected.\n\n```\nRefine-created entries (3):\n\n  name                           age  ver     uses  recurred  verdict\n  gmail-scope-fix                12d   v2        5        no  working\n      expects: Gmail sends stop returning insufficient_scope\n  prisma-migrate-note             9d   v1       ~0         —  too early\n      expects: —\n  bash-path-hint                  3d   v3        2       yes  did not help\n      expects: PATH errors stop appearing before shell commands\n\nCandidates for removal:\n  bash-path-hint — /refine rollback 8c1d2e3f4a5b\n```\n\nThe audit deletes nothing. It prints a rollback command only for recorded\ncandidates. Skill rows keep their plain names; memory and prompt-note rows use\n`memory:` / `prompt:` prefixes so same-named entries remain distinguishable.\nEvery row shows the model's sanitized expected outcome (`—` when omitted)\nalongside its observed result. Later edits of the same entry advance a version;\nversion 3 or later is labelled `churning` only when the normal verdict would\notherwise be `unclear`. Skills that remain unused are fed into later proposals\nas negative examples.\n\nTwo honesty rules behind the verdicts:\n\n- **`no recurrence window`** — the pattern table had no post-edit rows at all\n(typically after a restored or rebuilt`state.db` ). An empty scan cannot\ntell \"the failure stopped\" from \"the evidence was lost\", so the row names\nthe gap instead of drifting into`unclear` or claiming`working` .\n- **Recurrence horizon** (`refine.audit_recurrence_horizon_days` , also accepted\nas`refine.recurrence_horizon_days` , default**3** ).\nOn the reference journal, the median gap between recurrences of a chronic\nfailure is minutes and the 95th percentile is 2.17 days — so silence shorter\nthan the horizon is indistinguishable from a pause. Fingerprintless rows\n(no recurrence signal at all) earn`working` only after`age >= horizon` ;\nedits younger than that stay`too early` . Raise the key only if your\nfailures genuinely pause longer than that; the default is measured, not\nguessed. This horizon governs recurrence verdicts only —`unused_skills` '\nseparate`min_age_days` (14) answers a different question (\"has the skill\nbeen left idle\") and is unchanged.\n- **A kind with no usage counter still earns `working` — on recurrence alone.** The host counts uses only for skills, so`uses` is structurally unavailable for\nmemory entries and prompt notes. Until recently that made`working` unreachable\nfor them: the branch required`uses > 0` , so the edit kind refine produces most\noften could never be reported as successful however long it held, and the column\nread`unclear` forever. Recurrence now carries the verdict alone for those kinds,\nunder the same bar the usage path uses and not a lower one — the silence must be*measured* (`recurred` false, never unmeasured), a fingerprint must exist (with\nneither a fingerprint nor a counter there is no evidence at all, and the row stays`unclear` ), and the edit must be older than the recurrence horizon. The row still\nprints`uses` as`—` , so it stays visible which evidence carried the verdict. The\ngate is on**kind** , not on`usage_scope` : a skill whose usage lookup merely*failed* also reports`unavailable` , and that is an unmeasured dimension rather\nthan an absent one, so it does not borrow this path.\n- **Memory rows check presence, not usage.** The host keeps no usage counter\nfor memory entries, so the only checkable fact for an applied memory edit is\nwhether the exact content refine appended is still in the store. Exact\nmembership cannot tell an edit from a removal — both make the string\ndisappear — so when the content is gone the verdict is`unreliable — no longer present as applied` , never \"was deleted\". If the\nhost memory state cannot be read at all, the row says`unreliable — target state unavailable` rather than guessing.\n\nThe agent gets a `refine_run` tool (toolset `refine`) and may trigger the same\nserialized flow with an optional `reason`. It also accepts `session_id` for one\nexact historical session and `dry_run: true` to preview without applying. The\nhandler validates an explicit session against the read-only sessions table\nbefore any model call and forwards all three arguments to `core.refine_run`.\n\nThe tool must run inside an active Hermes gateway turn: it reuses the\nhost-provided `ctx.llm`, which carries that turn's active runtime routing. An\nexternal script that constructs `PluginLlm(plugin_id=\"refine\")` is not\nequivalent; outside a gateway turn it can fall back to a configured provider\ninstead of the active model.\n\nBy default refine inherits the user's **live main model**. Hermes resolves the\nmodel inside its own `call_llm`: with no explicit provider/model it takes the\n`auto` path, whose first step is \"main provider + main model\", and the main\nmodel is read from a process-local runtime override that the agent refreshes at\nthe top of every turn. So a model switched mid-session is intended to apply to\nrefine as well, without any plugin-side plumbing.\n\nOne caveat is worth knowing, and it depends on the Hermes version. On Hermes\nbuilds older than 2026-07-17, auxiliary clients are cached under a key that does\n**not** include the resolved model; a plugin call passes no live-runtime dict, so\nthe key is constant and the first cached client keeps supplying the model\ncaptured when it was built, outliving a mid-session switch until the entry is\nevicted or the process restarts. Upstream closed this in `73057ed16`\n(\"scope runtime state to each turn\") and `fdc6c32d7` (\"isolate runtime cache by\nlive context\"), both dated 2026-07-17 — verified by reading the Hermes repository,\nnot from this one, so re-check against your own checkout before relying on it.\nNever a refine bug either way; on an older host, restarting the gateway clears it.\n\n`/refine model` reports which source **refine** resolved, and with `source: live`\nthe value it read from the host at that moment. It cannot report which model a\ncached host client will actually use, so on an older host it is not a way to\nconfirm a mid-session switch took effect. Restart the gateway, or pin the target.\n\nPinning refine's own target sidesteps all of that and makes the choice deterministic:\n\n```\nplugins:\n  entries:\n    refine:\n      llm:\n        allow_provider_override: true   # required for `provider` below\n        allow_model_override: true      # required for `model` below\n        provider: your-provider\n        model: your-cheap-model\n```\n\nModel availability depends on provider, account, and region. A `403 RegionError`\nmeans the provider received the request and refused that model — commonly an\naccount or region restriction that needs an explicit opt-in with the provider.\nBecause refine inherits the live main model, a restricted main model makes refine\nfail for as long as the main model does; the fix is to opt in or select an\navailable model with `hermes model`, not to pin refine elsewhere. Check\n`llm_meta.reported_provider` and `reported_model` to see which target was\nactually refused.\n\nBoth `allow_*` flags are fail-closed in Hermes: with them off, a pinned value is\nrefused rather than applied. Leave `provider`/` model` unset to inherit the live\nmain model as described above. Every path — the `/refine` command, the\n`refine_run` tool, and both automatic triggers — shares the one host-provided\nclient and honors this setting identically.\n\nAll keys live under `plugins.entries.refine`:\n\n| Key | Type | Default | Description | \n|---|---|---|---|\n| `auto_enabled` | bool | `true` | Enable automatic turn and session-end attempts. Forced off when the Hermes config cannot be read. | \n| `auto_min_messages` | int | `15` | Minimum messages for session-end auto-analysis. | \n| `auto_turn_interval` | int | `25` | Assistant messages added since this session's last automatic attempt; `0` disables only the turn trigger. | \n| `auto_cooldown_minutes` | int | `20` | Minimum durable journal-derived gap between automatic attempts. | \n| `notify_enabled` | bool | `true` | Notify the active chat after an edit is applied; notification failure never changes the refine outcome. | \n| `notify_target` | str | unset | Explicit Hermes delivery target used when no active chat is available. There is deliberately no implicit platform target. | \n| `max_edits_per_run` | int | `1` | Maximum proposal passes per run. | \n| `max_edits_per_proposal` | int | `3` | Maximum inseparable edits one proposal may apply as a single transaction. `1` disables transactions. | \n| `max_edits_per_day` | int | `3` | Maximum applied, pending, prepared, rollback-prepared, or pending-rollback **edits** per UTC day. This is the blast-radius limit and is re-checked before every edit. | \n| `only_agent_created` | bool | `true` | Only patch agent-created skills. | \n| `journal_dir` | path | `<HERMES_HOME>/refine` | Journal, lock, ledger, backups, prompt notes, and the `/refine model` override. An empty value uses this default. | \n| `overview_max_entries` | int | `40` | Existing skills and memory snippets listed per kind in a proposal prompt. | \n| `overview_max_chars` | int | `240` | Maximum characters in each structured overview or history line. | \n| `history_max_entries` | int | `20` | Recent create/patch outcomes fed back into a proposal prompt. | \n| `min_signal_required` | bool | `true` | Require a signal before the proposal call; may enable reviewer fallback. | \n| `min_pattern_count` | int | `2` | Repeats before a failure counts as a mechanical signal. | \n| `apply_min_sessions` | int | `2` | Distinct sessions required before a proposed edit may be applied. | \n| `apply_min_occurrences` | int | `5` | Failure occurrences required before a proposed edit may be applied. | \n| `reviewer_fallback_enabled` | bool | `true` | Allow one reviewer call when the mechanical gate finds nothing; its approved proposal is advisory and is never applied. | \n| `reviewer_min_messages` | int | `20` | Minimum session size for reviewer fallback. | \n| `reviewer_cooldown_minutes` | int | `60` | Minimum durable gap between reviewer decisions. | \n| `proposer_subagent_enabled` | bool | `true` | Produce proposals via a read-only subagent that can open skill bodies before deciding. Requires a bound parent turn; without one the structured call is the fallback either way. | \n| `proposer_subagent_strict` | bool | `false` | Make a subagent failure a journaled `subagent_strict_error` instead of silently downgrading to the structured call. | \n| `proposer_subagent_timeout_seconds` | int | `180` | Wall-clock bound on the subagent proposal wait (minimum 5). The structured-call and reviewer timeouts are constants in `llm.py` (`_PROPOSAL_TIMEOUT_SECONDS` ,`_REVIEW_TIMEOUT_SECONDS` , both 180 s) and are not configurable. All three describe the same piece of work and are deliberately the same number. | \n| `prompt_notes_enabled` | bool | `true` | Permit `prompt` proposals and note injection. | \n| `prompt_notes_max_count` | int | `5` | Maximum active notes injected into one turn. | \n| `prompt_notes_max_chars` | int | `600` | Maximum characters in the complete injected note block. | \n| `prompt_notes_default_scope` | str | `global` | Scope for newly created prompt notes: `global` or`session` ; invalid values fall back to`global` . | \n| `cross_session_enabled` | bool | `true` | Aggregate failures across recent sessions. | \n| `skip_session_sources` | list[str] | `[\"cron\"]` | Skip matching session sources before any trajectory messages are read; each skip is journaled without consuming edit budget. | \n| `cross_session_days` | int | `7` | Interactive cross-session look-back window. | \n| `cross_session_max_sessions` | int | `25` | Interactive session scan cap. | \n| `cross_session_max_rows` | int | `4000` | Maximum trajectory rows scanned by an interactive cross-session pass. | \n| `dedup_window_days` | int | `7` | Refuse an edit identical to a recent applied, pending, or prepared edit. | \n| `audit_recurrence_horizon_days` | int | `3` | Days of post-edit silence after which `/refine audit` reads \"no recurrence\" as fixed rather than paused. Also accepted as`recurrence_horizon_days` ; the explicit`audit_` key wins when both are set. | \n\nLLM trust policy (`plugins.entries.refine.llm`):\n\n```\nllm:\n  allow_model_override: false\n  allow_provider_override: false\n```\n\n- \n**No plugin-level post-compaction hook:** Hermes exposes no normal plugin hook\nfor`session:compress` ; that event is gateway-only.`on_session_reset` is\nused to expire session-scoped notes, not as a claim that refinement runs after\ncontext compaction. The only plugin-side compaction registration,`register_context_engine` , replaces Hermes's built-in`ContextCompressor` and\npermits only one engine per install. Taking it over would make**Refine Cycle** responsible for the agent's whole compaction strategy and conflict with any\nreal context-engine plugin. A safe integration needs an observer-only`VALID_HOOKS` member fired at the compaction boundary.\n- \n**No plugin-level reasoning-effort control:** Hermes's structured plugin call\nexposes no provider reasoning/thinking setting. A model that returns only\nreasoning and no final text is reported as`llm_incomplete` ; pin a\nnon-reasoning model for refine with`plugins.entries.refine.llm` (`model` /`provider` ) under the existing trust policy when that mitigation is needed.\n- \n**A model switch can be masked by Hermes's auxiliary client cache, on older\nhosts only:** plugin calls resolve through the`auto` path, which prefers the\nlive main model, but before`73057ed16` /`fdc6c32d7` (both 2026-07-17) the\nclient cache key omitted the resolved model and a plugin call supplied no\nlive-runtime dict, so the key never changed and a cached client kept its\noriginal model until eviction or restart. Refine cannot close this from the\nplugin side and does not try, and it cannot detect which host version it runs\non, so`/refine model` cannot tell you whether you are affected. On a current\nhost it is fixed; otherwise restart the gateway or pin`llm.model` /`llm.provider` .\n- \n**The live main model is read through a private host API:**`live_main_target()` imports`_read_main_provider` /`_read_main_model` from`agent.auxiliary_client` . Hermes exposes no public accessor. Both names were\nconfirmed present in a real installation, but a private name can move without\nnotice, so the import is guarded and simply yields no live value on failure —`/refine model` then reports`source: host_default` rather than claiming a\ntarget it does not have.\n- \n**Text-only trust boundary:**`PluginLlmTextInput` accepts text but no typed\ntrust level. Refine wraps and scrubs untrusted trajectory content, which is a\nmitigation rather than hard separation; a guarantee requires a typed\ntrust-level input from Hermes.\n- \n**Approval terminal states are not exported:** the plugin can observe pending\nwrites and reconcile the target, but Hermes does not expose distinct`accepted` ,`rejected` , and`cancelled` terminal states.\n- \n**Exact timestamped usage is unavailable:** existing SQL and host counters are\napproximate. Reliable`working` /`unused` conclusions require timestamped\nusage events from Hermes.\n- \n**PrimeIntellect comparison was not completed during the audit:** access to\nthe required network/source material was blocked, so no equivalence claim is\nmade.\n- \n**Production frequency and storage growth are unmeasured:** the audit did not\nread the real`state.db` ; it therefore makes no claim about production event\nfrequency or long-term storage growth.\n- \n**No host approval for the prompt-note store:** it is a plugin-owned atomic\nfile, not a host memory or skill write. Host-managed skill and memory changes\nstill respect staged approvals and reconciliation.\n- \n**A session note that stops matching its cleanup intent has no terminal\nstate:** if the note store is hand-edited or a note is moved to another scope\nor session after`cleanup_prepared` was journaled, the note is retained and\nthe entry stays`cleanup_prepared` — non-terminal, and not reversible, because\nthe artifact rollback would remove is not the one the entry describes. Every\nlater end or reset of that same session id reports it again by note id. A\nterminal state would have to keep counting against the daily budget (the edit\ndid happen) and needs its own crash-ordering matrix, so it is deliberately\nleft as a design decision rather than approximated. Repairing or removing the\noffending entry in`prompt_notes.json` by hand clears it.\n- \n**Rollback is not modeled as an ordinary proposal:** rolling back a skill`create` means deleting it, and the no-delete guardrail rejects any proposal\ncarrying a delete. Routing rollback through the proposal path would therefore\nneed a privileged bypass of that guardrail. It would also replace the`rollback_prepared` /`pending_rollback` /`rolled_back` transitions that\napproval reconciliation and`/refine rollback <id>` idempotence depend on, and\nbreak rollback for every record written before the change. Rollback keeps its\nown path; what it gained is journal snapshots, so it no longer depends on a\nfile surviving on disk.\n- \n**`hermes plugins remove` fails on Windows for git-managed plugins (host\ndefect, not this repo's code):** the CLI removes the directory with a bare`shutil.rmtree` that does not handle read-only files, and git marks`.git/objects/*` read-only. Removal aborts midway with`WinError 5` , leaving a\nhalf-deleted directory; runtime data and`config.yaml` are untouched.\nWorkaround: delete the directory from PowerShell\n(`Remove-Item -Recurse -Force` ) or clear the read-only attribute first. An\nupstream`onerror` handler that clears the bit and retries would fix it\nproperly.\n\nA successful mutation returns a rollback command only when its journal record is actually reversible:\n\n```\n/refine rollback <journal_id>\n```\n\nCreate rollback deletes a skill only if current content still exactly matches the refine proposal. Patch rollback refuses to overwrite a later change before restoring its pre-edit content. Memory rollback removes only the exact appended entry and preserves unrelated later entries. Prompt-note rollback removes only its exact unchanged note and preserves later notes; a changed or missing note is a conflict and is left untouched.\n\nA skill patch records its pre-edit content twice: as a `snapshot` inside the\njournal record, and as a `.bak` file under `journal_dir/backups`. Both come from\none host read, so they cannot disagree. Rollback prefers the snapshot, so losing\nthe backup file no longer costs the rollback.\n\nCredential scrubbing needs two layers here, because the journal redacts\ncredentials from everything it writes — including a snapshot. The first layer is\nthe proposal path: a skill whose current `SKILL.md` is changed by scrubbing is\nnever patched at all, and the patch becomes a `no_op` before the model is\ncalled. The second is a SHA-256 digest of the real pre-edit content stored beside\nthe snapshot. If the stored text no longer matches that digest, the snapshot is\nrefused and the raw `.bak` file is used instead, so redacted text is never\nwritten over a skill.\n\n`is_reversible` asks the restore path the same question rollback does, so an\nentry is never advertised as reversible when neither source survives. In that\ncase rollback refuses with an explicit error and changes nothing, and a staged\nrollback whose state cannot be proven stays `pending_rollback` rather than being\ndeclared rejected.\n\nRecords written before snapshots existed carry only `backup_path` and keep\nrolling back from it unchanged.\n\nEach edit of a multi-edit transaction owns its own journal record and its own\nrollback ID; there is no transaction-level undo. Recovery IDs are listed\n**newest first**, which is the order to follow: memory recovery is positional, so\nundoing an earlier append before a later one shifts the later entry and its\nrollback fails closed as a conflict.\n\nIf mutation succeeded but journal finalization failed, the returned recovery ID\npoints to the durable `prepared` record. Pending forward approvals consume budget\nbut are not advertised as reversible until the target exactly matches the\nproposal. Rollback intent is journaled before its side effect; a staged rollback\nreturns a pending ID and is not called rolled back until the target change is\nconfirmed. A rejected rollback returns the entry to `applied`, so it can be\nretried.\n\n```\ncd <HERMES_HOME>/plugins/refine\npython -m tests.run_tests\n```\n\nThe regression suite uses only the Python standard library and a fake Hermes\nhost. It installs that fake host before importing the plugin.\nEvery database, journal, backup, skill, memory file, ledger, and lock lives\nunder a fresh `TemporaryDirectory`; running the suite cannot touch live Hermes\nor profile state. It covers proposal completion, host action mapping,\nbackup/journal failures, create/patch/memory/prompt rollback conflicts, secret\nsanitation, approval reconciliation, automatic triggers and cooldowns, reviewer\nfallback, prompt-note injection and scope cleanup, append-only journal recovery,\nand full-history aggregation.\n\nThe suite also starts two real Python processes against one temporary Hermes\nroot using a filesystem rendezvous and bounded timeouts. With\n`max_edits_per_day: 1`, it proves exactly one mutation is applied, one budget\nslot is consumed, and one ledger/skill record survives.\n\nRuntime modules and installation assets live at the repository root; the\ninstaller copies the shipped plugin subset to `<HERMES_HOME>/plugins/refine/`.\n\n```\nRefine-Cycle-for-Hermes-Agent/\n├── plugin.yaml          # Hermes plugin manifest\n├── __init__.py          # command, tool, and hook registration\n├── config.py            # plugins.entries.refine config reader\n├── core.py              # evidence, guardrails, serialized apply orchestration\n├── sanitization.py      # recursive credential redaction\n├── patterns.py          # normalization, fingerprints, aggregation, signal gate\n├── ledger.py            # timestamp-aware usefulness ledger and audit report\n├── llm.py               # structured proposal, reviewer, and patch regeneration\n├── journal.py           # append-only journal, lock, notes, recovery, rollback\n├── notify.py            # failure-isolated applied-edit notification delivery\n├── refine_trace.py      # synthetic trace helper shipped with the plugin\n├── install.py           # cross-platform full installer, status, and rollback\n├── install.sh           # Linux host-route patch helper only\n├── assets/              # bundled invocation-route patches and README media\n└── tests/\n    └── run_tests.py     # hermetic regression and cross-process proof\n```\n\nRefine sends sanitized aggregated error patterns, explicit correction excerpts,\na bounded structured overview of existing skills (name, description, category,\nand a known local version) and memory snippets, the optional manual\nreason/prior-pass note, and up to 8,000 characters of sanitized recent\ntrajectory to the configured provider. Each overview line is bounded by\n`overview_max_chars`; each kind is capped by `overview_max_entries`, with a\nvisible `+N more` marker. It also sends up to `history_max_entries` of its own\nmost recent create/patch outcomes, including expected outcomes, so prior results\ncan inform the next proposal. Empty history sends no history block; the existing\nnegative examples for unused skills remain separate.\n\nIf the mechanical signal gate has no signal, the reviewer receives only the\nbounded sanitized trajectory and returns a tiny verdict. When a skill patch is\nselected, a second structured request receives the target's current complete\n`SKILL.md` only if it is safe and no larger than the shared 15,000-character\ninput/output limit. The proposal budget derives from that limit locally because\nHermes exposes no model output-limit capability. Unsafe or oversized current\nskill content becomes `no_op`; it is never redacted, truncated, or used to\ngenerate a destructive replacement.\n\nCredentials are redacted first, but remaining content is ordinary conversation\nor skill content. Automatic analysis is on by default; set\n`auto_enabled: false` if model-bound session analysis must be manually\ninitiated.\n\n- **Credential scrubbing** covers evidence, reasons, proposals, reviewer\nverdicts, host errors, prompt notes, and recursively nested journal fields.\n- **Stale-plan guard** — a skill patch proposal carries a SHA-256 baseline\ndigest captured at planning time. Before backup, and again against the\nrecovery snapshot captured for rollback, the plugin re-reads the live host\nstate and refuses the edit with a non-budget-consuming`conflict` journal\noutcome when the literal content has already changed, disappeared, or cannot\nbe read reliably. Host preprocessing is disabled for these reads so inline\nshell directives are not executed and cannot alter the baseline. Transaction\npreflight applies zero edits when any target is stale at that point.\nProposals without a baseline (manually assembled or legacy) bypass this check\nunchanged. Hermes does not expose an atomic compare-and-write operation:\nanother process can still race the final check and host write, and an approved\nstaged write can race changes made while approval is pending. A transaction\ncan therefore become partial if a target changes after preflight.\n- **Signal gate and reviewer** reject one-off noise; reviewer failures and\nmalformed output decline safely without a proposal call.\n- **Incomplete model replies are visible:** a malformed, token-limited, or\nreasoning-only reply becomes a non-budget-consuming`llm_incomplete` journal\noutcome, never a false \"nothing to propose\" result.\n- **Shared proposal limit:** the proposal token budget derives from the\n15,000-character content guardrail, while the reviewer uses its independent\n2,400-token cap (raised from 300 after measurement: a reasoning model spent\nthe whole 300 thinking and returned no verdict at all).\n- **Agent-created skills only** for patches; creates require a free normalized\nname and cannot use the reserved`hermes-` prefix.\n- **No autonomous skill delete** — skill deletion is used only by an explicit\nrollback of an unchanged skill created by refine.\n- **Bounded ephemeral prompt context** is labelled, sanitized, whole-note\nbounded, scoped, and never changes the base system prompt.\n- **Serialized budget** counts applied, pending-approval, and unresolved\nprepared records after acquiring the process-safe mutation lock. Lock\nacquisition is bounded for both in-process and cross-process contention, so a\ncontended run reports a timeout instead of hanging its caller.\n- **Durable append journal** writes one locked, fsynced JSON line per state\ntransition without rewriting history. A corrupt trailing line is skipped and\nisolated before the next valid record; backup, ledger, and note-store writes\nare atomic.\n- **Conflict-aware rollback** preserves later skill, memory, and prompt-note\nchanges.\n- **Host approval reconciliation** handles staged skill and memory writes when a\nmanaged or re-enabled gate remains active. By default, registration attempts\nto turn both host write-approval gates off; plugin-owned prompt notes never use\na host approval queue.\n- **Read-only trajectory** —`state.db` is opened with`mode=ro` .\n- **No system prompt access** — the base prompt stays immutable.\n- **Host support.** Uses the plugin API available since Hermes 0.17.0\n(`register_tool` ,`register_command` ,`register_hook` ,`ctx.llm` ). Verified on**0.19.0** (server, patched core),**0.20.1** (desktop, stock core), and**0.20.2** (subagent proposal path end to end). New\nproposals additionally need the host route patch (see Installation); without it\nthey fail loudly with`llm_invocation_unavailable` , which is the intended honest\ngate. The manifest format cannot express a host requirement, so this is enforced\nat runtime rather than at install time. On**0.21.0** the plugin installs, loads,\nregisters, and proposes:`assets/invocation-route-v0.21.0.patch` applies, and\nfour proposals on a real 0.21.0 host reached the session's own model — see[Hermes version support](#hermes-version-support) .\n\nRefine Cycle has not been through a single validation pass. It has been through a long programme of them: synthetic scenario matrices run and re-run across many configurations, replays over corpora of real recorded conversations, ablations that put the shipped defaults against wider alternatives, and clean installs on both Linux and Windows hosts. That work is what the design rests on.\n\n**It does not damage anything.** Sessions where writing nothing is the correct\nbehaviour receive no writes. Every rollback restores its target byte for byte.\nLive memory, journal and configuration are untouched by the runs themselves,\nverified by hashing before and after rather than assumed.\n\n**It refuses instead of guessing.** When the evidence is thin, the reply is\nmalformed, or a proposal is not grounded in a real recurring failure, the run\nends in a journaled refusal. Nothing is ever reported as applied that was not\napplied.\n\n**It reaches the right model, and the loop closes.** On both a Linux and a\nWindows host, live runs went to the exact model of the active session — one\nrequest each, no substitution, no silent fallback to something cheaper. On a\ncurrent desktop host the whole cycle then ran end to end on a real session: the\nrecurrence gate opened, the model returned one grounded proposal, and the\njournal recorded `prepared`, `applied`, `rollback_prepared` and `rolled_back`\nin turn — with the note gone from the store afterwards and the usefulness ledger\ncarrying the edit, its fingerprint and its final outcome.\n\n**It learns from real conversations, not only from scenarios.** Replayed over a\ncorpus of recorded sessions on the current build, the plugin produced grounded,\napplicable lessons on roughly half of the sessions that carried a genuine\nrepeated failure — each naming the specific failure it was drawn from — and\nwrote nothing at all on the matched clean sessions, where writing nothing is the\ncorrect behaviour. An earlier build produced none of them: the proposal model\nkept omitting the fingerprint the apply bar requires, so every candidate was\nrefused rather than written. That defect is gone.\n\n**The defaults are set by evidence, not by taste.** Ablations compared the\nshipped configuration against wider ones. Showing the proposer every eligible\nfailure pattern instead of the strongest few made it measurably worse, so the\nnarrower default stayed.\n\n**It was audited continuously, not signed off once.** Review ran the length of\nthe project rather than at the end of it: numbered rounds into the teens, each\nfinding reproduced and specced before anything was changed, and the four\n`FINDING-*.md` documents in `docs/` are the ones still worth keeping after their\nfixes landed. Fifty-two of this repository's first 539 commits carry a finding,\naudit, review or round in their subject line.\n\nMost of those audits were run by agents that had also written the code, which is\nthe weakest kind. So one was deliberately handed to a model with no part in\nwriting it and no access to the authors' reasoning: it produced five hypotheses,\nall five held on inspection, and all five are fixed with regression tests proven\nto fail on the parent commit and pass after — see\n[`docs/INDEPENDENT-REVIEW.md`](/Bergschloss/Refine-Cycle-for-Hermes-Agent/blob/main/docs/INDEPENDENT-REVIEW.md). Two of them (a\npoisoned timestamp consuming the whole query budget; a session-scoped rule\nenforced against every session) had survived every self-review before it.\n\n**It holds under its own suite.** The full plugin suite passes on Linux and\nWindows across supported Python versions, on every commit.\n\nEverything above describes what the testing establishes. This section is about the remaining edges — where confidence rests on construction and tests rather than on accumulated use in the field.\n\n- **Crash behaviour is tested by its consequences, not by killing a process.** Partial journal tails (including a crash inside the plugin's own append, between\nthe bytes landing and`fsync` returning), interrupted staging, stuck`prepared` records, abandoned rollbacks, and stale cross-process locks left by a dead owner\nall have tests. What has never been done is pulling power from a real host\nmid-write and observing recovery on the resulting state.\n\nThat is not a known defect. It is simply the place where the evidence is tests rather than mileage, named here rather than left for a user to find.\n\nMIT © 2026 Taras Boiko", "url": "https://wpnews.pro/news/refine-cycle-self-improvement-plugin-for-hermes-agent", "canonical_source": "https://github.com/Bergschloss/Refine-Cycle-for-Hermes-Agent", "published_at": "2026-09-08 07:54:48+00:00", "updated_at": "2026-09-08 08:02:20.650186+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-research"], "entities": ["Refine Cycle", "Hermes Agent", "Nous Research", "Prime Intellect", "Prime Agent"], "alternates": {"html": "https://wpnews.pro/news/refine-cycle-self-improvement-plugin-for-hermes-agent", "markdown": "https://wpnews.pro/news/refine-cycle-self-improvement-plugin-for-hermes-agent.md", "text": "https://wpnews.pro/news/refine-cycle-self-improvement-plugin-for-hermes-agent.txt", "jsonld": "https://wpnews.pro/news/refine-cycle-self-improvement-plugin-for-hermes-agent.jsonld"}}