revert(gateway): keep profile truncation routing out of scope

This commit is contained in:
fangliquanflq
2026-08-16 01:11:29 +08:00
committed by Teknium
parent 0640fe7119
commit 3863de3155
2 changed files with 42 additions and 110 deletions
-67
View File
@@ -5291,73 +5291,6 @@ def test_prompt_submit_truncates_by_row_id(monkeypatch):
server._sessions.pop("row-id-trunc-sid", None)
def test_prompt_submit_row_id_truncates_profile_owned_history(monkeypatch, tmp_path):
"""A remote-profile edit must write through the same DB used to resolve it."""
from hermes_state import SessionDB
profile_home = tmp_path / "remote-profile"
profile_home.mkdir()
session_key = "profile-row-id-session"
db = SessionDB(db_path=profile_home / "state.db")
try:
db.create_session(session_key, source="desktop")
db.append_messages_batch(
session_key,
[
{"role": "user", "content": "first"},
{"role": "assistant", "content": "reply 1"},
{"role": "user", "content": "second"},
{"role": "assistant", "content": "reply 2"},
],
)
history = db.get_messages_as_conversation(
session_key, include_row_ids=True
)
target_row_id = history[2]["_row_id"]
finally:
db.close()
sess = _session(history=list(history), session_key=session_key)
sess["profile_home"] = str(profile_home)
server._sessions["profile-row-id-sid"] = sess
monkeypatch.setattr(
server,
"_get_db",
lambda: pytest.fail("must not write through the launch-profile DB"),
)
monkeypatch.setattr(server, "_start_agent_build", lambda *a, **k: None)
try:
resp = server.handle_request(
{
"id": "1",
"method": "prompt.submit",
"params": {
"session_id": "profile-row-id-sid",
"text": "edited second",
"truncate_before_row_id": target_row_id,
"confirm_truncate": True,
},
}
)
assert resp.get("error") is None
assert [message["content"] for message in sess["history"]] == [
"first",
"reply 1",
]
verify_db = SessionDB(db_path=profile_home / "state.db")
try:
persisted = verify_db.get_messages_as_conversation(session_key)
finally:
verify_db.close()
assert [message["content"] for message in persisted] == [
"first",
"reply 1",
]
finally:
server._sessions.pop("profile-row-id-sid", None)
def test_prompt_submit_truncates_by_string_row_id(monkeypatch):
"""#82959: String row IDs in history match correctly against integer truncate_before_row_id."""
replaced = []
+42 -43
View File
@@ -575,49 +575,48 @@ def _(rid, params: dict) -> dict:
# new exchange is appended on top of the "undone" turns — durable
# zombie history on resume, and the edit/regenerate never sticks.
# Fail closed: refuse the turn and leave memory/DB unchanged.
with _session_db(session) as db:
if db is not None:
try:
# active_only=True: replace only the live (active=1) rows.
# In-place compaction (#38763) keeps the pre-compaction
# transcript as active=0/compacted=1 rows under this same
# session key; a bare replace_messages() would DELETE that
# durable archive on every edit/regenerate — the same bug
# class #80216 fixed for /retry. On an uncompacted session
# all rows are active=1, so this is behaviorally identical
# to the full replace.
# archive_dropped: a rewind overwrites turns the user may
# not have meant to drop, and this write is the last step
# before they are gone — three reported incidents ended
# here with nothing to restore from (#70516, #80763,
# #82756). Soft-archiving keeps them on disk (active=0) and
# in the FTS index, so a mis-aimed cut is recoverable
# instead of terminal. The live transcript is unchanged.
# Fall back to session id when session_key is NULL — CLI-origin
# sessions created before the session_key default fix have no
# key, and replace_messages(None) triggers an FK violation.
truncation_key = session.get("session_key") or sid
db.replace_messages(
truncation_key,
truncated,
active_only=True,
archive_dropped=True,
)
except Exception as exc:
logger.error(
"prompt.submit: replace_messages failed for session %s "
"(ordinal=%d); refusing turn so memory and DB stay "
"aligned: %s",
sid,
ordinal,
exc,
exc_info=True,
)
return _err(
rid,
5008,
f"failed to persist history truncation: {exc}",
)
if (db := _get_db()) is not None:
try:
# active_only=True: replace only the live (active=1) rows.
# In-place compaction (#38763) keeps the pre-compaction
# transcript as active=0/compacted=1 rows under this same
# session key; a bare replace_messages() would DELETE that
# durable archive on every edit/regenerate — the same bug
# class #80216 fixed for /retry. On an uncompacted session
# all rows are active=1, so this is behaviorally identical
# to the full replace.
# archive_dropped: a rewind overwrites turns the user may
# not have meant to drop, and this write is the last step
# before they are gone — three reported incidents ended
# here with nothing to restore from (#70516, #80763,
# #82756). Soft-archiving keeps them on disk (active=0) and
# in the FTS index, so a mis-aimed cut is recoverable
# instead of terminal. The live transcript is unchanged.
# Fall back to session id when session_key is NULL — CLI-origin
# sessions created before the session_key default fix have no
# key, and replace_messages(None) triggers an FK violation.
truncation_key = session.get("session_key") or sid
db.replace_messages(
truncation_key,
truncated,
active_only=True,
archive_dropped=True,
)
except Exception as exc:
logger.error(
"prompt.submit: replace_messages failed for session %s "
"(ordinal=%d); refusing turn so memory and DB stay "
"aligned: %s",
sid,
ordinal,
exc,
exc_info=True,
)
return _err(
rid,
5008,
f"failed to persist history truncation: {exc}",
)
session["history"] = truncated
session["history_version"] = int(session.get("history_version", 0)) + 1
if db is not None: