refactor(memory,skills): replace tri-state write_mode with boolean write_approval (default off) (#43354)

The shipped tri-state write_mode (on|off|approve) conflated two concepts —
whether writes are enabled and whether they're gated — so 'on' (writes flow
freely, gate inactive) read like 'gating is on'. Replace it with a single
clear boolean gate that defaults off.

  memory.write_approval / skills.write_approval:
    false (default) — write freely; the approval gate is off (pre-gate behaviour)
    true            — require approval: memory foreground prompts inline, memory
                      background-review + all skill writes stage for review

The old 'off = block all writes' mode is dropped; memory_enabled: false already
disables memory entirely, so a third 'block' state was redundant.

- tools/write_approval.py: get_write_mode/MODE_* → write_approval_enabled() bool;
  evaluate_gate() loses the config-driven 'blocked' path (blocked now only comes
  from an interactive user denial).
- tools/memory_tool.py, tools/skill_manager_tool.py: comment + behaviour follow.
- hermes_cli/config.py: memory/skills write_mode → write_approval (False);
  _config_version 28→29 with a 28→29 migration that renames any persisted
  write_mode (approve→true, on/off/unset→false) and drops the old key.
- slash commands: '/memory|/skills mode <on|off|approve>' → 'approval <on|off>'
  ('mode' kept as a back-compat alias); set_mode_fn callback now takes a bool.
- write_approval_commands.py, cli_commands_mixin.py, gateway/slash_commands.py,
  commands.py: handlers + registry args/subcommands updated.
- docs + tests rewritten for the boolean model; added migration tests.
This commit is contained in:
Teknium
2026-06-09 23:21:14 -07:00
committed by GitHub
parent 9ca9697342
commit 095f526b11
11 changed files with 296 additions and 187 deletions
+20 -19
View File
@@ -209,21 +209,22 @@ memory:
user_profile_enabled: true
memory_char_limit: 2200 # ~800 tokens
user_char_limit: 1375 # ~500 tokens
write_mode: on # on | off | approve
write_approval: false # false = write freely (default) | true = require approval
```
## Controlling memory writes (`write_mode`)
## Controlling memory writes (`write_approval`)
By default the agent saves memory freely — including from the background
self-improvement review that runs after a turn. If you'd rather not have
memory written behind your back, `memory.write_mode` gives you three options
(applied to **both** foreground turns and the background review):
self-improvement review that runs after a turn. If you'd rather approve saves
first, set `memory.write_approval: true`. It's a simple on/off gate applied to
**both** foreground turns and the background review:
| Mode | Behaviour |
|------|-----------|
| `on` | Write freely (default — current behaviour). |
| `off` | Never write. The memory tool returns a clean "disabled" result; nothing is saved. |
| `approve` | Don't commit writes — review them first. Foreground writes prompt you inline (entries are small enough to read in a chat bubble). Background-review writes are **staged** instead of committed (a background thread can't block on a prompt). |
| `write_approval` | Behaviour |
|------------------|-----------|
| `false` (default) | Write freely — the gate is off (the pre-gate behaviour). |
| `true` | Require approval before anything is saved. Foreground writes prompt you inline (entries are small enough to read in a chat bubble). Background-review writes are **staged** instead of committed (a background thread can't block on a prompt). |
> To turn memory off entirely (not just gate it), set `memory_enabled: false`.
Review staged writes from the CLI or any messaging platform:
@@ -231,33 +232,33 @@ Review staged writes from the CLI or any messaging platform:
/memory pending # list staged memory writes (auto ones tagged [auto])
/memory approve <id> # apply one (or 'all')
/memory reject <id> # drop one (or 'all')
/memory mode approve # change write_mode and persist it
/memory approval on # turn the gate on (or 'off') and persist it
```
This is the answer to "the agent saved a wrong assumption about me": set
`write_mode: approve`, and every save — especially the unprompted background
`write_approval: true`, and every save — especially the unprompted background
ones — waits for your yes/no before it ever enters your profile.
## Controlling skill writes (`skills.write_mode`)
## Controlling skill writes (`skills.write_approval`)
Skills use the same three-state gate, but the review UX differs because a
Skills use the same on/off gate, but the review UX differs because a
`SKILL.md` is far too large to read in a chat bubble:
```yaml
skills:
write_mode: on # on | off | approve
write_approval: false # false = write freely (default) | true = require approval
```
In `approve` mode, skill writes (create / edit / patch / write_file / delete)
always **stage** regardless of origin. You review the one-line gist inline, but
the full diff stays out-of-band:
When `write_approval: true`, skill writes (create / edit / patch / write_file /
delete) always **stage** regardless of origin. You review the one-line gist
inline, but the full diff stays out-of-band:
```
/skills pending # list staged skill writes + a one-line gist each
/skills diff <id> # full unified diff (best viewed in CLI or dashboard)
/skills approve <id> # apply it (or 'all')
/skills reject <id> # drop it (or 'all')
/skills mode approve # change write_mode and persist it
/skills approval on # turn the gate on (or 'off') and persist it
```
On a messaging platform, approve a skill from its gist + metadata, or open