From b09ed52c84db853f4751665c036ba745a1c2800b Mon Sep 17 00:00:00 2001 From: JoaoMarcos44 Date: Fri, 25 Sep 2026 23:50:24 -0300 Subject: [PATCH] fix(sessions): make prompt repair atomic (cherry picked from commit 55bcd8f6ccd667a5352769ad82bcbad1e73f82b1) --- hermes_cli/sessions_cmd.py | 6 +++--- hermes_cli/subcommands/sessions.py | 4 ++-- hermes_state_sessions.py | 19 +++++++++++++++++++ .../test_sessions_repair_prompts.py | 17 +++++++++++++++++ website/docs/reference/cli-commands.md | 2 +- website/docs/user-guide/sessions.md | 3 ++- 6 files changed, 44 insertions(+), 7 deletions(-) diff --git a/hermes_cli/sessions_cmd.py b/hermes_cli/sessions_cmd.py index 264a9cf41f..ba4d004bf7 100644 --- a/hermes_cli/sessions_cmd.py +++ b/hermes_cli/sessions_cmd.py @@ -1135,9 +1135,9 @@ def _cmd_repair_prompts(db, args): cleared = [] for finding in findings: - db.update_system_prompt(finding["id"], None) - if finding["clear_pin"]: - db.update_session_tool_names(finding["id"], None) + db.clear_system_prompt_for_rebuild( + finding["id"], clear_tool_names=finding["clear_pin"], + ) cleared.append(finding["id"]) if as_json: diff --git a/hermes_cli/subcommands/sessions.py b/hermes_cli/subcommands/sessions.py index d17335bb30..d820dd97a1 100644 --- a/hermes_cli/subcommands/sessions.py +++ b/hermes_cli/subcommands/sessions.py @@ -231,12 +231,12 @@ def build_sessions_parser(subparsers, *, cmd_sessions: Callable) -> None: "Automatic repair requires positive tools[] evidence; rows without a readable pin are " "reported as unverifiable and never changed by a scan. Clearing a prompt makes the next " "turn rebuild and persist healthy bytes. Reports without touching anything unless --apply " - "is given; a session_id explicitly targets one row regardless of detector evidence.") + "is given; a session_id is an explicit destructive override and can clear even a healthy prompt.") _flag(sessions_repair_prompts, "--apply", help="Clear the verified prompts (default: report only)") _flag(sessions_repair_prompts, "--json", help="Machine-readable output; with --apply, apply without an interactive confirmation") sessions_repair_prompts.add_argument("session_id", nargs="?", default=None, - help="Session id (or unique prefix) to clear explicitly, skipping detector evidence") + help="Destructive override: clear this session even when its stored prompt is healthy") sessions_recover = sessions_subparsers.add_parser( "recover", help="Rebuild canonical session data into a separate clean database", diff --git a/hermes_state_sessions.py b/hermes_state_sessions.py index 74c861e660..82f3851b82 100644 --- a/hermes_state_sessions.py +++ b/hermes_state_sessions.py @@ -673,6 +673,25 @@ class SessionSessionsMixin: self._delete_unreferenced_system_prompts(conn) self._execute_write(_do) + def clear_system_prompt_for_rebuild( + self, session_id: str, *, clear_tool_names: bool = False, + ) -> None: + """Atomically clear a stored prompt and, when requested, its tools[] pin for a fresh rebuild.""" + def _do(conn): + if clear_tool_names: + conn.execute( + "UPDATE sessions SET system_prompt_hash = NULL, system_prompt = NULL, " + "tool_names = NULL WHERE id = ?", + (session_id,), + ) + else: + conn.execute( + "UPDATE sessions SET system_prompt_hash = NULL, system_prompt = NULL WHERE id = ?", + (session_id,), + ) + self._delete_unreferenced_system_prompts(conn) + self._execute_write(_do) + def update_session_model( self, session_id: str, model: str, provider: Optional[str] = None, *, base_url: Optional[str] = None, api_mode: Optional[str] = None, diff --git a/tests/hermes_cli/test_sessions_repair_prompts.py b/tests/hermes_cli/test_sessions_repair_prompts.py index 945ba19e37..e53b4369e3 100644 --- a/tests/hermes_cli/test_sessions_repair_prompts.py +++ b/tests/hermes_cli/test_sessions_repair_prompts.py @@ -122,6 +122,23 @@ def test_reduced_surface_left_alone_memory_only_pin_cleared(db, monkeypatch, cap assert not (memory_row["tool_names"] or "") +def test_prompt_and_memory_pin_clear_roll_back_together_on_failure(db, monkeypatch): + target = db.create_session("atomic-repair", "telegram", system_prompt=DEGRADED) + db.update_session_tool_names(target, _pin("memory")) + + def _fail_gc(_conn): + raise RuntimeError("forced repair settlement failure") + + with monkeypatch.context() as patch: + patch.setattr(db, "_delete_unreferenced_system_prompts", _fail_gc) + with pytest.raises(RuntimeError, match="forced repair settlement failure"): + db.clear_system_prompt_for_rebuild(target, clear_tool_names=True) + + row = db.get_session(target) + assert row["system_prompt"] == DEGRADED + assert _repair_prompts_pin_names(row) == ["memory"] + + def test_positional_target_clears_memory_pin_when_prompt_is_already_null(db, monkeypatch): target = db.create_session("target-null", "telegram") db.update_session_tool_names(target, _pin("memory")) diff --git a/website/docs/reference/cli-commands.md b/website/docs/reference/cli-commands.md index ce7771ecf4..7cc34062ff 100644 --- a/website/docs/reference/cli-commands.md +++ b/website/docs/reference/cli-commands.md @@ -1760,7 +1760,7 @@ Subcommands: | `optimize-storage` | Migrate the full-text search index to the compact v23 external-content layout; on large databases this reclaims a large fraction of `state.db`. | | `repair` | Repair a malformed `state.db` schema (e.g. `table messages_fts already exists`) so hidden sessions reappear; a backup is made first. | | `repair-routing` | Re-attach gateway conversations stranded in session rows that lost their routing identity (a chat "jumping back in time" after a restart). Dry-run by default; `--apply` performs the adoptions (stop the gateway first); `--max-gap-seconds N` tunes the contiguity window. Only unambiguous cases are repaired. See [Sessions → Repair Stranded Gateway Sessions](../user-guide/sessions.md#repair-stranded-gateway-sessions). | -| `repair-prompts` | Report stored system prompts provably degraded by the pre-#122822 maintenance-compaction bug. Report-only by default; `--apply` clears verified rows so the next turn rebuilds them, `--json` is machine-readable (and non-interactive when combined with `--apply`), and an explicit `session_id` targets one row. Rows without a readable tools[] pin are reported as unverifiable and never auto-repaired. See [Sessions → Repair Degraded Stored Prompts](../user-guide/sessions.md#repair-degraded-stored-prompts). | +| `repair-prompts` | Report stored system prompts provably degraded by the pre-#122822 maintenance-compaction bug. Report-only by default; `--apply` clears verified rows so the next turn rebuilds them, `--json` is machine-readable (and non-interactive when combined with `--apply`), and an explicit `session_id` is a destructive override that can clear even a healthy prompt. Rows without a readable tools[] pin are reported as unverifiable and never auto-repaired. See [Sessions → Repair Degraded Stored Prompts](../user-guide/sessions.md#repair-degraded-stored-prompts). | | `repair-profiles` | Settle session, routing, Telegram-topic and voice-mode state that landed under the wrong profile (rows in another profile's store, labels disagreeing with the session key, parent links crossing profiles, index rows for deleted profiles). Dry-run by default; `--apply` performs the repairs after snapshotting every store (stop the gateway first); `--legacy-main rekey\|move` decides what `agent:main` rows inside a named profile's store are; `--json` for automation. See [Sessions → Repair State Crossed Between Profiles](../user-guide/sessions.md#repair-state-crossed-between-profiles). | | `recover` | Offline, non-destructive recovery of a damaged `state.db` into a separate clean database. | | `retitle-skills` | Regenerate titles for sessions opened with a `/skill`, using what the user actually typed; lists changes unless `--apply` is passed. | diff --git a/website/docs/user-guide/sessions.md b/website/docs/user-guide/sessions.md index a596131291..c03b7a616a 100644 --- a/website/docs/user-guide/sessions.md +++ b/website/docs/user-guide/sessions.md @@ -684,7 +684,8 @@ hermes sessions repair-prompts --json # Non-interactive automation: apply and report the ids actually cleared hermes sessions repair-prompts --apply --json -# Explicit operator override for one session (id or unique prefix) +# Explicit destructive override for one session (id or unique prefix). +# This clears the stored prompt even when it is healthy. hermes sessions repair-prompts SESSION_ID --apply ```