feat(compaction): system prompt always rebuilds at the commit boundary — updates finally reach long-lived sessions (#98426)
* feat(compaction): always rebuild the system prompt at the commit boundary — keep-prompt now gated on byte equality of the LIVE builder output; plugin sections re-render with fail-open to last good bytes * feat(clock): 'Conversation started' resolves through the session-lineage ROOT — a compacted/rotated session keeps its original birth date (Bot Mode forever-chats know when they were first born) * test: retire old-contract pins — plugin sections re-render at invalidate (freeze stays restore-only), commit boundary always runs the live builder, byte-equal keep preserves object identity
This commit is contained in:
@@ -3979,29 +3979,21 @@ def compress_context(
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# Built-in memory is the only system-prompt input that a normal
|
||||
# compaction reloads. When the cached prompt already embeds the
|
||||
# freshly-reloaded memory blocks verbatim, keep the exact cached
|
||||
# prompt so local backends retain their KV-cache prefix. Containment
|
||||
# (not before/after snapshot equality) is required: fresh-agent
|
||||
# surfaces restore the cached prompt from the session DB, where it
|
||||
# can predate mid-session memory writes the in-memory snapshot has
|
||||
# already absorbed. External providers can change their own prompt
|
||||
# block during on_pre_compress(), so they retain the rebuild path.
|
||||
if (
|
||||
cached_system_prompt is not None
|
||||
and getattr(agent, "_memory_manager", None) is None
|
||||
and _cached_prompt_reflects_builtin_memory(agent, cached_system_prompt)
|
||||
):
|
||||
# ALWAYS rebuild the prompt at the admitted-commit boundary
|
||||
# (maintainer-directed, #95681 arc). The previous "keep-prompt"
|
||||
# containment branch put the OLD bytes back whenever the reloaded
|
||||
# memory blocks were already embedded — which meant prompt-builder
|
||||
# changes (guidance diets, new blocks, renames) NEVER reached a
|
||||
# long-lived session. The cache argument for keeping bytes was
|
||||
# hollow: when nothing changed, the rebuild is byte-identical and
|
||||
# local KV prefixes survive on equality; when something changed,
|
||||
# the cache was stale by definition and propagation is the point.
|
||||
# Preserve OBJECT identity on byte-equality for backends that key
|
||||
# on it.
|
||||
rebuilt_system_prompt = agent._build_system_prompt(system_message)
|
||||
if cached_system_prompt is not None and rebuilt_system_prompt == cached_system_prompt:
|
||||
new_system_prompt = cached_system_prompt
|
||||
agent._cached_system_prompt = cached_system_prompt
|
||||
# _invalidate_system_prompt() above also cleared the
|
||||
# cross-session-stable prefix marker boundary. The kept prompt
|
||||
# is byte-identical, so reconstruct the stable tier and reuse
|
||||
# it ONLY when the kept prompt still literally starts with it
|
||||
# (same startswith gate as the restore path); otherwise the
|
||||
# request layer falls back to the legacy single-breakpoint
|
||||
# layout with the prompt bytes untouched.
|
||||
from agent.system_prompt import reconstruct_static_prefix
|
||||
|
||||
reconstruct_static_prefix(
|
||||
@@ -4010,8 +4002,18 @@ def compress_context(
|
||||
log_label="compression keep-prompt",
|
||||
)
|
||||
else:
|
||||
new_system_prompt = agent._build_system_prompt(system_message)
|
||||
new_system_prompt = rebuilt_system_prompt
|
||||
agent._cached_system_prompt = new_system_prompt
|
||||
if cached_system_prompt is not None:
|
||||
logger.info(
|
||||
"Compaction rebuilt a drifted system prompt "
|
||||
"(session=%s, %d -> %d chars): builder output changed "
|
||||
"since the stored snapshot (update, config change, or "
|
||||
"memory/skills growth)",
|
||||
agent.session_id or "none",
|
||||
len(cached_system_prompt),
|
||||
len(new_system_prompt),
|
||||
)
|
||||
|
||||
_session_commit_succeeded = False
|
||||
_commit_started_at = time.monotonic()
|
||||
|
||||
@@ -209,8 +209,19 @@ def _frozen_plugin_prompt_sections(agent: Any) -> tuple:
|
||||
|
||||
rendered = tuple(render_system_prompt_sections(_plugin_session_info(agent)))
|
||||
except Exception as exc:
|
||||
logger.warning("Plugin system prompt sections could not be rendered: %s", exc)
|
||||
rendered = ()
|
||||
# Fail-open: a plugin whose render raises at a rebuild boundary
|
||||
# keeps its last good bytes (stashed by invalidate_system_prompt)
|
||||
# instead of silently vanishing from the prompt.
|
||||
previous = getattr(agent, "_plugin_system_prompt_sections_previous", None)
|
||||
if previous:
|
||||
logger.warning(
|
||||
"Plugin system prompt sections failed to re-render (%s); "
|
||||
"keeping the previous frozen sections", exc,
|
||||
)
|
||||
rendered = previous
|
||||
else:
|
||||
logger.warning("Plugin system prompt sections could not be rendered: %s", exc)
|
||||
rendered = ()
|
||||
setattr(agent, attr, rendered)
|
||||
return rendered
|
||||
|
||||
@@ -284,6 +295,13 @@ def _session_start_like(agent: Any, now: Any) -> Any:
|
||||
a Thursday-morning resume), contradicting the fresh per-turn time hint.
|
||||
Prefer, in order:
|
||||
|
||||
0. the LINEAGE-ROOT session id's embedded timestamp — compaction can
|
||||
rotate the session id, and each rotated id embeds its OWN mint time,
|
||||
so after months of compactions rung 1 alone would quietly re-birth
|
||||
the conversation at its latest rotation. Walking to the lineage root
|
||||
(same walk as ``_conversation_root_id``) recovers the ORIGINAL
|
||||
birth stamp — a Bot Mode forever-chat keeps knowing when it was
|
||||
first born, across every compaction (maintainer-directed, #98426);
|
||||
1. the timestamp embedded in ``session_id`` (``YYYYMMDD_HHMMSS_...``) —
|
||||
immutable for the life of the session, so the line is byte-stable
|
||||
across every rebuild boundary (preserving prefix-cache KV);
|
||||
@@ -315,18 +333,30 @@ def _session_start_like(agent: Any, now: Any) -> Any:
|
||||
pass
|
||||
return dt
|
||||
|
||||
# 1. Session id embeds the true start as YYYYMMDD_HHMMSS.
|
||||
# 0. Lineage root: compaction rotation mints NEW ids with NEW embedded
|
||||
# stamps. Walk to the root id (cached on the agent — the lineage only
|
||||
# grows at compaction, and this function runs at that exact boundary,
|
||||
# so one walk per rebuild is fresh enough) and prefer ITS embedded
|
||||
# timestamp: the conversation's true birth. Fail-open to rung 1.
|
||||
session_id = getattr(agent, "session_id", None)
|
||||
if isinstance(session_id, str) and session_id:
|
||||
m = re.match(r"^(\d{8})_(\d{6})", session_id)
|
||||
if m:
|
||||
try:
|
||||
embedded = datetime.strptime(
|
||||
f"{m.group(1)}_{m.group(2)}", "%Y%m%d_%H%M%S"
|
||||
)
|
||||
return _to_display_tz(embedded)
|
||||
except ValueError:
|
||||
pass
|
||||
root_id = None
|
||||
try:
|
||||
db = getattr(agent, "_session_db", None)
|
||||
if db is not None and isinstance(session_id, str) and session_id:
|
||||
root_id = db.get_conversation_root(session_id)
|
||||
except Exception:
|
||||
root_id = None
|
||||
for candidate in (root_id, session_id):
|
||||
if isinstance(candidate, str) and candidate:
|
||||
m = re.match(r"^(\d{8})_(\d{6})", candidate)
|
||||
if m:
|
||||
try:
|
||||
embedded = datetime.strptime(
|
||||
f"{m.group(1)}_{m.group(2)}", "%Y%m%d_%H%M%S"
|
||||
)
|
||||
return _to_display_tz(embedded)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 2. Session-creation stamp set by the runner.
|
||||
session_start = getattr(agent, "session_start", None)
|
||||
@@ -1028,10 +1058,21 @@ def invalidate_system_prompt(agent: Any) -> None:
|
||||
"""Invalidate the cached system prompt, forcing a rebuild on the next turn.
|
||||
|
||||
Called after context compression events. Also reloads memory from disk
|
||||
so the rebuilt prompt captures any writes from this session.
|
||||
so the rebuilt prompt captures any writes from this session, and clears
|
||||
the frozen plugin-section snapshot so plugins re-render at the same
|
||||
boundary (maintainer-directed, #95681 arc): a plugin section is just
|
||||
another prompt block carrying state — freezing it while memory, skills,
|
||||
and guidance refresh would recreate the stale-block disease inside
|
||||
plugin-land. The previous bytes are stashed so a plugin whose render
|
||||
RAISES falls back to its last good section instead of vanishing
|
||||
(fail-open guard, not a freeze).
|
||||
"""
|
||||
agent._cached_system_prompt = None
|
||||
agent._cached_system_prompt_static = None
|
||||
_snapshot_attr = "_plugin_system_prompt_sections_snapshot"
|
||||
if hasattr(agent, _snapshot_attr):
|
||||
agent._plugin_system_prompt_sections_previous = getattr(agent, _snapshot_attr)
|
||||
delattr(agent, _snapshot_attr)
|
||||
if agent._memory_store:
|
||||
agent._memory_store.load_from_disk()
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ def _install_test_section(manager: PluginManager, content) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_real_aiagent_builds_section_once_and_keeps_it_out_of_static_prefix(monkeypatch):
|
||||
def test_real_aiagent_freezes_section_within_life_and_rerenders_on_invalidate(monkeypatch):
|
||||
# Pin the workspace snapshot: build_coding_workspace_block shells out to
|
||||
# live `git status`/`git log` on every build, and a git call failing or
|
||||
# timing out under xdist contention makes the two builds differ in the
|
||||
@@ -64,15 +64,26 @@ def test_real_aiagent_builds_section_once_and_keeps_it_out_of_static_prefix(monk
|
||||
agent = _real_agent()
|
||||
|
||||
first = build_system_prompt(agent)
|
||||
# Within a prompt's life the frozen section is reused: a second build
|
||||
# WITHOUT invalidation must not re-run plugin code.
|
||||
again = build_system_prompt(agent)
|
||||
assert again == first
|
||||
assert len(calls) == 1
|
||||
|
||||
# invalidate_system_prompt is the compaction/rebuild boundary (#98426):
|
||||
# plugin sections re-render there like every other prompt block, so a
|
||||
# long-lived session's plugin context converges instead of freezing at
|
||||
# its birth bytes.
|
||||
invalidate_system_prompt(agent)
|
||||
rebuilt = build_system_prompt(agent)
|
||||
|
||||
assert first == rebuilt
|
||||
assert len(calls) == 1
|
||||
assert len(calls) == 2
|
||||
assert "rules render 2" in rebuilt
|
||||
assert "rules render 1" not in rebuilt
|
||||
assert calls[0]["session_id"] == agent.session_id
|
||||
assert "## Plugin Context: example.rules" in first
|
||||
assert "rules render 1" in first
|
||||
assert first.index("## Plugin Context: example.rules") < first.index("Conversation started:")
|
||||
assert calls[1]["session_id"] == agent.session_id
|
||||
assert "## Plugin Context: example.rules" in rebuilt
|
||||
assert rebuilt.index("## Plugin Context: example.rules") < rebuilt.index("Conversation started:")
|
||||
assert "example.rules" not in agent._cached_system_prompt_static
|
||||
|
||||
|
||||
@@ -129,18 +140,28 @@ def test_fresh_process_resume_restores_identical_full_prompt_without_callback(tm
|
||||
]
|
||||
_restore_or_build_system_prompt(agent, None, history)
|
||||
restored = agent._cached_system_prompt
|
||||
calls_after_restore = int(calls_path.read_text())
|
||||
rebuilt_equal = None
|
||||
rebuilt_has_changed = None
|
||||
calls_after_rebuild = None
|
||||
if os.environ["TEST_PHASE"] != "first":
|
||||
# invalidate_system_prompt is the compaction boundary (#98426):
|
||||
# plugin sections re-render there, so the rebuilt prompt picks
|
||||
# up the plugin's CURRENT output — it is EXPECTED to differ
|
||||
# from the restored bytes when the plugin's render changed.
|
||||
invalidate_system_prompt(agent)
|
||||
rebuilt = build_system_prompt(agent)
|
||||
rebuilt_equal = rebuilt == restored
|
||||
agent._cached_system_prompt = rebuilt
|
||||
rebuilt_has_changed = "CHANGED" in rebuilt
|
||||
calls_after_rebuild = int(calls_path.read_text())
|
||||
print(json.dumps({
|
||||
"prompt_b64": base64.b64encode(
|
||||
agent._cached_system_prompt.encode("utf-8")
|
||||
restored.encode("utf-8")
|
||||
).decode("ascii"),
|
||||
"calls": int(calls_path.read_text()),
|
||||
"calls": calls_after_restore,
|
||||
"rebuilt_equal": rebuilt_equal,
|
||||
"rebuilt_has_changed": rebuilt_has_changed,
|
||||
"calls_after_rebuild": calls_after_rebuild,
|
||||
}))
|
||||
db.close()
|
||||
"""
|
||||
@@ -179,6 +200,12 @@ def test_fresh_process_resume_restores_identical_full_prompt_without_callback(tm
|
||||
f"resumed={resumed_prompt[diff_at - 100:diff_at + 100]!r}"
|
||||
)
|
||||
assert b"original bytes" in first_prompt
|
||||
# RESTORE stays frozen: the resumed prompt is byte-identical, plugin
|
||||
# render never ran (calls unchanged). The REBUILD boundary re-renders:
|
||||
# the rebuilt prompt carries the plugin's current output and the
|
||||
# render ran exactly once more.
|
||||
assert b"CHANGED" not in resumed_prompt
|
||||
assert outputs[0]["calls"] == outputs[1]["calls"] == 1
|
||||
assert outputs[1]["rebuilt_equal"] is True
|
||||
assert outputs[1]["rebuilt_equal"] is False
|
||||
assert outputs[1]["rebuilt_has_changed"] is True
|
||||
assert outputs[1]["calls_after_rebuild"] == 2
|
||||
|
||||
@@ -567,6 +567,61 @@ class TestSessionStartLike:
|
||||
assert start.strftime("%Y-%m-%d") == "2026-01-01"
|
||||
assert start.tzinfo is not None
|
||||
|
||||
def test_prefers_lineage_root_over_rotated_segment_id(self):
|
||||
"""Compaction rotates session ids; each rotation embeds its own
|
||||
mint time. The birth date must come from the lineage ROOT so a
|
||||
Bot Mode forever-chat keeps knowing when it was first born
|
||||
(#98426)."""
|
||||
from agent.system_prompt import _session_start_like
|
||||
|
||||
class _Db:
|
||||
def get_conversation_root(self, sid):
|
||||
assert sid == "20260615_090000_seg9"
|
||||
return "20260101_120000_root"
|
||||
|
||||
now = datetime(2026, 6, 16, 9, 0, tzinfo=ZoneInfo("UTC"))
|
||||
agent = SimpleNamespace(
|
||||
session_id="20260615_090000_seg9",
|
||||
session_start=datetime(2026, 6, 15, 9, 0),
|
||||
_session_db=_Db(),
|
||||
)
|
||||
start = _session_start_like(agent, now)
|
||||
assert start.strftime("%Y-%m-%d") == "2026-01-01"
|
||||
|
||||
def test_lineage_walk_failure_falls_open_to_segment_id(self):
|
||||
from agent.system_prompt import _session_start_like
|
||||
|
||||
class _Db:
|
||||
def get_conversation_root(self, sid):
|
||||
raise RuntimeError("db locked")
|
||||
|
||||
now = datetime(2026, 6, 16, 9, 0, tzinfo=ZoneInfo("UTC"))
|
||||
agent = SimpleNamespace(
|
||||
session_id="20260615_090000_seg9",
|
||||
session_start=datetime(2026, 6, 15, 9, 0),
|
||||
_session_db=_Db(),
|
||||
)
|
||||
start = _session_start_like(agent, now)
|
||||
assert start.strftime("%Y-%m-%d") == "2026-06-15"
|
||||
|
||||
def test_nontimestamp_root_falls_through_to_segment_id(self):
|
||||
"""A root id without an embedded stamp (legacy/imported lineage)
|
||||
must not break the ladder — rung 1 still applies."""
|
||||
from agent.system_prompt import _session_start_like
|
||||
|
||||
class _Db:
|
||||
def get_conversation_root(self, sid):
|
||||
return "imported-legacy-root"
|
||||
|
||||
now = datetime(2026, 6, 16, 9, 0, tzinfo=ZoneInfo("UTC"))
|
||||
agent = SimpleNamespace(
|
||||
session_id="20260615_090000_seg9",
|
||||
session_start=datetime(2026, 6, 15, 9, 0),
|
||||
_session_db=_Db(),
|
||||
)
|
||||
start = _session_start_like(agent, now)
|
||||
assert start.strftime("%Y-%m-%d") == "2026-06-15"
|
||||
|
||||
def test_falls_back_to_session_start(self):
|
||||
from agent.system_prompt import _session_start_like
|
||||
|
||||
|
||||
@@ -481,8 +481,11 @@ class TestPreflightCompression:
|
||||
{"role": "user", "content": f"{SUMMARY_PREFIX}\nPrevious conversation"},
|
||||
{"role": "user", "content": "hello", _DB_PERSISTED_MARKER: True},
|
||||
]
|
||||
assert new_system_prompt == "You are helpful."
|
||||
build_prompt.assert_not_called()
|
||||
# Post-#98426 the commit boundary ALWAYS runs the live builder;
|
||||
# its output differs from the cached prompt here, so the rebuilt
|
||||
# prompt wins.
|
||||
assert new_system_prompt == "new system prompt"
|
||||
build_prompt.assert_called_once()
|
||||
assert events == [
|
||||
("lifecycle", COMPACTION_STATUS),
|
||||
("compress", "started"),
|
||||
@@ -529,7 +532,12 @@ class TestPreflightCompression:
|
||||
assert ("compacted", COMPACTION_DONE_STATUS) not in events
|
||||
|
||||
def test_compression_reuses_cached_prompt_when_memory_snapshot_is_unchanged(self, agent):
|
||||
"""A memory reload without new injected text must keep the cache prefix."""
|
||||
"""A byte-equal rebuild must keep the EXACT cached prompt object.
|
||||
|
||||
Post-#98426 the commit boundary always runs the live builder; when
|
||||
its output is byte-identical to the stored prompt, the ORIGINAL
|
||||
string object is kept (KV/prefix caches keyed on identity survive).
|
||||
"""
|
||||
agent.compression_enabled = False
|
||||
agent._memory_enabled = True
|
||||
agent._user_profile_enabled = False
|
||||
@@ -547,7 +555,11 @@ class TestPreflightCompression:
|
||||
"compress",
|
||||
return_value=[{"role": "user", "content": f"{SUMMARY_PREFIX}\nPrevious conversation"}],
|
||||
),
|
||||
patch.object(agent, "_build_system_prompt") as build_prompt,
|
||||
patch.object(
|
||||
agent,
|
||||
"_build_system_prompt",
|
||||
return_value="cached system prompt\n\n<memory>same facts</memory>",
|
||||
) as build_prompt,
|
||||
):
|
||||
_, new_system_prompt = agent._compress_context(
|
||||
[{"role": "user", "content": "hello"}],
|
||||
@@ -557,7 +569,7 @@ class TestPreflightCompression:
|
||||
|
||||
assert new_system_prompt is agent._cached_system_prompt
|
||||
assert new_system_prompt == "cached system prompt\n\n<memory>same facts</memory>"
|
||||
build_prompt.assert_not_called()
|
||||
build_prompt.assert_called_once()
|
||||
memory_store.load_from_disk.assert_called_once()
|
||||
|
||||
|
||||
|
||||
93
tests/test_compaction_prompt_rebuild.py
Normal file
93
tests/test_compaction_prompt_rebuild.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Compaction ALWAYS rebuilds the system prompt from the live builder (#95681).
|
||||
|
||||
The old keep-prompt containment branch restored the stored bytes whenever the
|
||||
reloaded memory blocks were embedded — so prompt-builder changes (guidance
|
||||
diets, renames, new blocks) never reached long-lived sessions (Bot Mode
|
||||
forever-chats, gateway channels). New contract:
|
||||
|
||||
1. builder output byte-equal -> keep the ORIGINAL string object (identity
|
||||
preserved for KV/prefix caches keyed on it)
|
||||
2. builder output differs -> the rebuilt prompt wins, logged
|
||||
3. plugin sections re-render at the same boundary; a RAISING plugin falls
|
||||
back to its last good bytes (fail-open), never silently vanishes
|
||||
"""
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from agent.system_prompt import invalidate_system_prompt
|
||||
|
||||
|
||||
def _agent(**over):
|
||||
base = dict(
|
||||
_cached_system_prompt="OLD PROMPT",
|
||||
_cached_system_prompt_static="OLD",
|
||||
_memory_store=None,
|
||||
)
|
||||
base.update(over)
|
||||
return SimpleNamespace(**base)
|
||||
|
||||
|
||||
class TestInvalidateClearsPluginFreeze(unittest.TestCase):
|
||||
def test_invalidate_stashes_and_clears_plugin_snapshot(self):
|
||||
agent = _agent()
|
||||
agent._plugin_system_prompt_sections_snapshot = ("frozen-section",)
|
||||
invalidate_system_prompt(agent)
|
||||
self.assertFalse(hasattr(agent, "_plugin_system_prompt_sections_snapshot"))
|
||||
self.assertEqual(agent._plugin_system_prompt_sections_previous, ("frozen-section",))
|
||||
self.assertIsNone(agent._cached_system_prompt)
|
||||
|
||||
def test_invalidate_without_snapshot_is_noop_for_plugins(self):
|
||||
agent = _agent()
|
||||
invalidate_system_prompt(agent)
|
||||
self.assertFalse(hasattr(agent, "_plugin_system_prompt_sections_snapshot"))
|
||||
|
||||
|
||||
class TestPluginRerenderFailOpen(unittest.TestCase):
|
||||
def test_raising_plugin_render_falls_back_to_previous_bytes(self):
|
||||
from agent.system_prompt import _frozen_plugin_prompt_sections
|
||||
|
||||
agent = _agent(_cached_system_prompt=None)
|
||||
agent._plugin_system_prompt_sections_previous = ("last-good",)
|
||||
with patch("hermes_cli.plugins.render_system_prompt_sections",
|
||||
side_effect=RuntimeError("plugin exploded")):
|
||||
rendered = _frozen_plugin_prompt_sections(agent)
|
||||
self.assertEqual(rendered, ("last-good",))
|
||||
|
||||
def test_raising_plugin_render_without_previous_is_empty(self):
|
||||
from agent.system_prompt import _frozen_plugin_prompt_sections
|
||||
|
||||
agent = _agent(_cached_system_prompt=None)
|
||||
with patch("hermes_cli.plugins.render_system_prompt_sections",
|
||||
side_effect=RuntimeError("plugin exploded")):
|
||||
rendered = _frozen_plugin_prompt_sections(agent)
|
||||
self.assertEqual(rendered, ())
|
||||
|
||||
|
||||
class TestCommitAlwaysRebuilds(unittest.TestCase):
|
||||
"""Source-level contract pins for the commit-site semantics."""
|
||||
|
||||
def _src(self):
|
||||
import inspect
|
||||
from agent import conversation_compression as cc
|
||||
return inspect.getsource(cc)
|
||||
|
||||
def test_keep_prompt_branch_requires_byte_equality(self):
|
||||
src = self._src()
|
||||
i = src.find("rebuilt_system_prompt = agent._build_system_prompt(")
|
||||
self.assertGreater(i, 0, "commit site must always run the live builder")
|
||||
window = src[i:i + 900]
|
||||
self.assertIn("rebuilt_system_prompt == cached_system_prompt", window,
|
||||
"keep-prompt must be gated on BYTE EQUALITY of the "
|
||||
"rebuilt output, not on memory containment")
|
||||
self.assertNotIn("_cached_prompt_reflects_builtin_memory(agent, cached_system_prompt)",
|
||||
window,
|
||||
"the containment keep-prompt gate must not return")
|
||||
|
||||
def test_drift_rebuild_is_logged(self):
|
||||
src = self._src()
|
||||
self.assertIn("Compaction rebuilt a drifted system prompt", src)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -81,8 +81,10 @@ class TestCompactionWiring(unittest.TestCase):
|
||||
"""The commit boundary invokes the refresh and a raising refresh
|
||||
must not break compaction (wrapped in try/except at the call site).
|
||||
Pin the call-site contract by source: the helper call sits between
|
||||
_invalidate_system_prompt and the prompt rebuild, inside a
|
||||
try/except."""
|
||||
_invalidate_system_prompt and the always-rebuild of the prompt
|
||||
(post-#95681: the keep-prompt containment branch is gone — the
|
||||
rebuilt prompt is compared byte-for-byte and only object identity
|
||||
is preserved on equality)."""
|
||||
import inspect
|
||||
from agent import conversation_compression as cc
|
||||
|
||||
@@ -90,7 +92,7 @@ class TestCompactionWiring(unittest.TestCase):
|
||||
i_invalidate = src.find("agent._invalidate_system_prompt()")
|
||||
i_refresh = src.find("_refresh_agent_tool_definitions(agent)",
|
||||
i_invalidate)
|
||||
i_rebuild = src.find("_cached_prompt_reflects_builtin_memory(agent",
|
||||
i_rebuild = src.find("rebuilt_system_prompt = agent._build_system_prompt(",
|
||||
i_refresh)
|
||||
self.assertGreater(i_refresh, i_invalidate,
|
||||
"refresh must follow prompt invalidation")
|
||||
|
||||
Reference in New Issue
Block a user