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:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user