feat(agent): one-time protocol upgrade for legacy Bot Chat sessions

Bot Chats created before the epoch mechanism persisted prompts with no
protocol section and no stamp — the staleness check only fires on
stamped prompts, so pre-existing bots would never learn to message
teammates. stored_bot_chat_prompt_needs_upgrade() migrates them: one
rebuild, title-gated to Bot Chat, only when the probe would actually
emit a section (SOUL-append legacies and unmanaged installs are left
alone — rebuilding those would loop). The rebuilt prompt carries the
stamp, so the upgrade can never re-fire.

E2E v3b through the real restore path: legacy Bot Chat upgraded once
then verbatim-reused; legacy regular sessions byte-untouched.
tests/agent/ 4648/4648.
This commit is contained in:
Teknium
2026-08-16 12:51:55 -07:00
parent ea4310e76c
commit 4e22d070f4
3 changed files with 78 additions and 1 deletions
+20 -1
View File
@@ -790,7 +790,11 @@ def _restore_or_build_system_prompt(agent, system_message, conversation_history)
# fails closed to "reuse" so a probe failure can't burn cache.
_bot_stale = False
try:
from tools.bot_mode_probe import stored_prompt_capability_stale
from tools.bot_mode_probe import (
BOT_CHAT_TITLE,
stored_bot_chat_prompt_needs_upgrade,
stored_prompt_capability_stale,
)
_home_for_epoch = None
try:
@@ -800,6 +804,21 @@ def _restore_or_build_system_prompt(agent, system_message, conversation_history)
except Exception:
pass
_bot_stale = stored_prompt_capability_stale(stored_prompt, _home_for_epoch)
if not _bot_stale and getattr(agent, "_bot_mode_protocol", True):
# Legacy upgrade: a Bot Chat whose prompt predates the epoch
# mechanism (no stamp, no protocol) gets ONE migration
# rebuild — otherwise pre-existing bots would never learn
# the messaging protocol. Title-gated so ordinary unstamped
# sessions (i.e. all of them) never take this path; the
# rebuilt prompt carries the stamp, so it cannot re-fire.
_t = str(getattr(agent, "_session_title_hint", "") or "").strip()
if not _t and agent._session_db and agent.session_id:
try:
_t = str(agent._session_db.get_session_title(agent.session_id) or "").strip()
except Exception:
_t = ""
if _t == BOT_CHAT_TITLE:
_bot_stale = stored_bot_chat_prompt_needs_upgrade(stored_prompt, _home_for_epoch)
except Exception:
_bot_stale = False
if _bot_stale:
+31
View File
@@ -169,3 +169,34 @@ def test_stored_prompt_staleness(tmp_path):
# prompts without a stamp (every non-Bot-Chat session) are never stale
assert not bot_mode_probe.stored_prompt_capability_stale("ordinary prompt", home)
assert not bot_mode_probe.stored_prompt_capability_stale("", home)
def test_legacy_bot_chat_upgrade(tmp_path):
home = tmp_path / ".hermes"
home.mkdir()
_make_bot_profile(home, "researcher", managed=True)
legacy = "old prompt with no protocol and no stamp"
# legacy Bot Chat on a managed install → upgrade once
assert bot_mode_probe.stored_bot_chat_prompt_needs_upgrade(legacy, home)
# a rebuilt prompt (stamped) never re-fires
upgraded = legacy + "\n\n" + bot_mode_probe.get_bot_mode_protocol_section(home) + "\n\n" + bot_mode_probe.epoch_line(home)
assert not bot_mode_probe.stored_bot_chat_prompt_needs_upgrade(upgraded, home)
# SOUL already carries the legacy plugin-side append → probe silent →
# no upgrade (rebuilding would loop: the new prompt would be unstamped too)
bot_mode_probe._reset_cache_for_tests()
(home / "SOUL.md").write_text("# Me\n\n## Messaging other agents\nlegacy\n", encoding="utf-8")
assert not bot_mode_probe.stored_bot_chat_prompt_needs_upgrade(legacy, home)
# prompt whose SOUL section rode into it → protocol heading present → no upgrade
assert not bot_mode_probe.stored_bot_chat_prompt_needs_upgrade(
"prompt containing\n## Messaging other agents\nfrom SOUL", home
)
# unmanaged install → probe silent → never upgrades
bot_mode_probe._reset_cache_for_tests()
home2 = tmp_path / ".hermes2"
home2.mkdir()
assert not bot_mode_probe.stored_bot_chat_prompt_needs_upgrade(legacy, home2)
+27
View File
@@ -260,6 +260,33 @@ def stored_prompt_capability_stale(stored_prompt: str, home: str | os.PathLike |
return False
def stored_bot_chat_prompt_needs_upgrade(stored_prompt: str, home: str | os.PathLike | None = None) -> bool:
"""True when a Bot Chat session's stored prompt PREDATES this feature.
Legacy Bot Chats (created before bundling / this epoch mechanism)
persisted prompts with no protocol section and no epoch stamp; without
an explicit upgrade they would be stranded forever — the staleness check
above only fires on stamped prompts. This is a one-time migration per
legacy session: the caller must only invoke it for sessions titled
"Bot Chat", and only rebuilds when the probe would actually emit a
section (a profile whose SOUL.md already carries the legacy plugin-side
append keeps its protocol-free prompt — rebuilding those would loop,
since the probe stays silent and the rebuilt prompt would be unstamped
again). Fails closed to "no upgrade".
"""
try:
if _EPOCH_PREFIX in (stored_prompt or ""):
return False
if _PROTOCOL_HEADING in (stored_prompt or ""):
return False
# Only upgrade when the rebuild would actually add the section —
# this is what guarantees the rebuilt prompt carries a stamp and
# the upgrade can never re-fire.
return bool(get_bot_mode_protocol_section(home))
except Exception:
return False
def _reset_cache_for_tests() -> None:
with _lock:
_cached.clear()