From 8918e8a0fa3354be1ca2fdddc32f0b00c9b46cb2 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:07:24 +0530 Subject: [PATCH] fix(sessions): only auto-repair prompts with skill_manage evidence The repair-prompts detector cleared two legitimate prompts: - a pin with skills_list/skill_view but no skill_manage and zero skills installed: build_skills_system_prompt returns '' and SKILLS_GUIDANCE is only emitted with skill_manage, so the healthy prompt has neither marker; - an exact memory-only pin, which is also a user toolsets=[memory] config; the healthy rebuild was re-flagged on every run (not idempotent). Key the decision on the missing '## Skill Safety' guidance, which is unconditional when skill_manage is in the pin, and require skill_manage in the pin. Memory-only rows are now reported as unverifiable; the explicit SESSION_ID override still clears them and their pin. Docs: describe the tightened rule and note that a running gateway keeps cached prompts in memory, so it must be restarted after --apply. --- hermes_cli/sessions_cmd.py | 38 +++++++++++++------------- website/docs/reference/cli-commands.md | 2 +- website/docs/user-guide/sessions.md | 17 ++++++++---- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/hermes_cli/sessions_cmd.py b/hermes_cli/sessions_cmd.py index ba4d004bf7..c27cbf92f7 100644 --- a/hermes_cli/sessions_cmd.py +++ b/hermes_cli/sessions_cmd.py @@ -974,8 +974,6 @@ def _cmd_repair_routing(db, args): print(f"\nRepaired {repaired} of {len(adoptable)} session(s).") -_SKILL_TOOL_NAMES = frozenset({"skills_list", "skill_view", "skill_manage"}) -_SKILLS_INDEX_MARKER = "" _SKILLS_GUIDANCE_MARKER = "## Skill Safety" _HYGIENE_PIN_TOOLS = frozenset({"memory"}) @@ -1010,26 +1008,22 @@ def _repair_prompts_pin_names(row) -> list[str] | None: def _repair_prompts_missing_skills_markers(row) -> bool: + # Only the Skill Safety guidance is a reliable marker: is legitimately + # absent when no skills are installed, but the guidance is emitted whenever skill_manage is. prompt = (row.get("system_prompt") or "").strip() - return bool( - prompt - and _SKILLS_INDEX_MARKER not in prompt - and _SKILLS_GUIDANCE_MARKER not in prompt - ) + return bool(prompt and _SKILLS_GUIDANCE_MARKER not in prompt) def _repair_prompts_degraded_reason(row) -> str: - """Why the stored prompt is provably a reduced-toolset build; '' without sufficient evidence.""" + """Why the stored prompt is provably a reduced-toolset build; '' without sufficient evidence. + + A memory-only pin is not proof: toolsets=[memory] is also a legitimate user config whose + healthy prompt has no skills markers, so clearing it would repeat on every run. + """ if not _repair_prompts_missing_skills_markers(row): return "" - pin = _repair_prompts_pin_names(row) - if pin is None: - return "" - names = set(pin) - if any(name in names for name in _SKILL_TOOL_NAMES): - return "skills index missing while the tools[] pin carries skill tools" - if names == _HYGIENE_PIN_TOOLS: - return "skills index missing and the tools[] pin is the reduced memory-only set" + if "skill_manage" in (_repair_prompts_pin_names(row) or ()): + return "Skill Safety guidance missing while the tools[] pin carries skill_manage" return "" @@ -1086,10 +1080,16 @@ def _cmd_repair_prompts(db, args): "prompt_chars": len(row["system_prompt"] or ""), "clear_pin": set(pin or ()) == _HYGIENE_PIN_TOOLS, }) - elif _repair_prompts_missing_skills_markers(row) and pin is None: + elif _repair_prompts_missing_skills_markers(row) and ( + pin is None or set(pin) == _HYGIENE_PIN_TOOLS + ): unverifiable.append({ "id": row["id"], - "reason": "skills markers missing but the tools[] pin is unavailable or unreadable", + "reason": ( + "skills markers missing but the tools[] pin is unavailable or unreadable" + if pin is None else + "memory-only tools[] pin may be a user toolset; clear explicitly by SESSION_ID" + ), "prompt_chars": len(row["system_prompt"] or ""), }) @@ -1118,7 +1118,7 @@ def _cmd_repair_prompts(db, args): suffix = " (tools[] pin cleared too)" if finding["clear_pin"] else "" print(f" {finding['id']} ({finding['prompt_chars']} chars) - {finding['reason']}{suffix}") if unverifiable: - print(f"\nSkipped {len(unverifiable)} unverifiable row(s) with no readable tools[] pin.") + print(f"\nSkipped {len(unverifiable)} unverifiable row(s) without enough tools[] evidence.") if not apply: if as_json: diff --git a/website/docs/reference/cli-commands.md b/website/docs/reference/cli-commands.md index 7cc34062ff..f91f098ea6 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` 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-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, or with a memory-only pin, are reported as unverifiable and never auto-repaired. Restart a running gateway after `--apply` so repaired rows take effect. 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 c03b7a616a..778103063d 100644 --- a/website/docs/user-guide/sessions.md +++ b/website/docs/user-guide/sessions.md @@ -666,10 +666,11 @@ live session. After the root fix in PR #122825 is installed, use `hermes sessions repair-prompts` to find rows that were already degraded. The scan is conservative: it only proposes a repair when the stored prompt is -missing the skills markers **and** the persisted `tools[]` pin proves either -that skill tools belonged to the session or that the pin is exactly the -maintenance `memory`-only surface. Older or malformed rows with no readable -pin are reported as **unverifiable** and are never changed automatically. +missing the `## Skill Safety` guidance **and** the persisted `tools[]` pin +contains `skill_manage` (which always emits that guidance). Rows with no +readable pin, or with a `memory`-only pin (which is also a legitimate +`toolsets: [memory]` setup), are reported as **unverifiable** and are never +changed automatically; clear them explicitly by `SESSION_ID` if needed. ```bash # Report verified candidates and unverifiable rows; writes nothing @@ -689,8 +690,8 @@ hermes sessions repair-prompts --apply --json hermes sessions repair-prompts SESSION_ID --apply ``` -A verified memory-only `tools[]` pin is cleared together with the prompt so -the next live turn can pin the real surface again. Clearing the prompt +With an explicit `SESSION_ID`, a memory-only `tools[]` pin is cleared together +with the prompt so the next live turn can pin the real surface again. Clearing the prompt intentionally stores NULL; the next turn rebuilds and persists healthy bytes, which causes one expected `Stored system prompt ... is null; rebuilding from scratch` warning for each @@ -700,6 +701,10 @@ evidence of a new corruption. Run the repair only after the #122822 root fix is present; otherwise a later maintenance compaction can degrade the row again. +A running gateway keeps each cached session's old prompt in memory, so restart +the gateway after `--apply` (`hermes gateway restart`) for repaired rows to +take effect. + ### Repair State Crossed Between Profiles Every profile owns one `state.db`, and every gateway session key names the