mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
fix(api): find a reset local Bot Chat by its title for hermes peer dm
`hermes peer dm` looks up the peer's Bot Chat by exact title before every DM (`_find_bot_chat`). The listing shows a reset LOCAL lineage as its latest, untitled segment, so once the Bot Chat had been reset (the overflow auto-reset from the previous commit, or /new) the lookup came back empty. The client then tried to create a new Bot Chat, which collided with the title (HTTP 400, "Peer already has a 'Bot Chat' session but it is hidden ..."). An empty exact-title lookup now falls back to the titled creation id when it owns a reset local lineage, before the archive resurrection already there. The chat door admits that id onto the lineage's current segment. Live-pass finding on #140968 (package 10) Refs #134889 (LC-23) Refs #140719 (cherry picked from commit 652a6fc8545017bedca161543c23faed46ff87a7)
This commit is contained in:
1 parent
2e20b4de6e
commit
2cccb91d11
3 files changed
+34
-4
No files matched your search
@@ -141,6 +141,7 @@ from gateway.platforms import api_server_room_grants as _room_grants
|
||||
from gateway.platforms import api_server_runs as _api_runs
|
||||
from gateway.platforms.api_server_openai_routes import OpenAICompatRoutesMixin
|
||||
from gateway.platforms.api_server_memory_sessions import ApiServerMemorySessions
|
||||
from gateway.platforms.api_server_bot_chat import reset_local_title_owner
|
||||
from gateway.platforms.base import (
|
||||
MEDIA_TAG_CLEANUP_RE, BasePlatformAdapter, SendResult, _terminal_sentinel_start, is_network_accessible,
|
||||
validate_media_delivery_path)
|
||||
@@ -3036,7 +3037,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter):
|
||||
rows = [s for s in rows if (s.get("title") or "").strip() == title_filter]
|
||||
return rows
|
||||
|
||||
sessions = await _list()
|
||||
sessions = await _list() or (await reset_local_title_owner(db, title_filter, include_hidden))
|
||||
if title_filter and not sessions:
|
||||
# A canonical Bot Chat auto-archived by the orphan reaper would make `hermes peer dm`
|
||||
# mint transient sessions: resurrect and re-list; deliberate archives stay put.
|
||||
@@ -3057,8 +3058,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter):
|
||||
sessions = await _list()
|
||||
except Exception:
|
||||
pass # resolution degrades to today's no-row behavior
|
||||
# Back-filled pins arrive PAST the limit, so counting them would report
|
||||
# another page that doesn't exist. Only the recency window decides.
|
||||
# Back-filled pins arrive PAST the limit: counting them would report a page that doesn't exist.
|
||||
windowed = sum(1 for s in sessions if not s.get("pinned"))
|
||||
return web.json_response({
|
||||
"object": "list", "data": [self._session_response(s) for s in sessions],
|
||||
|
||||
@@ -84,6 +84,26 @@ def _refusal(authority, tip, exc):
|
||||
return reason, f'Bot Chat admission refused: {reason}'
|
||||
|
||||
|
||||
async def reset_local_title_owner(db, title, include_hidden):
|
||||
"""``[row]`` for the exact-title lookup of a LOCAL conversation that was reset (``/new``, the
|
||||
compression-exhaustion auto-reset), else ``[]``. The listing shows a reset lineage as its latest,
|
||||
untitled segment, so ``hermes peer dm`` stopped finding its Bot Chat and its create collided
|
||||
with the title. The titled creation id still owns the FIFO and is what the chat door admits."""
|
||||
return await asyncio.to_thread(_reset_local_title_owner, db, title, include_hidden) if title else []
|
||||
|
||||
|
||||
def _reset_local_title_owner(db, title, include_hidden):
|
||||
row = db.get_session_by_title(title)
|
||||
if row is None or row.get('end_reason') != 'session_reset' or (row.get('hidden') and not include_hidden):
|
||||
return []
|
||||
from hermes_state_local import local_receipt
|
||||
try:
|
||||
lineage = local_receipt(db, row['id']).get('lineage') or []
|
||||
except RuntimeStoreError:
|
||||
return []
|
||||
return [row] if len(lineage) > 1 else []
|
||||
|
||||
|
||||
def bot_chat_tip(authority, session_id):
|
||||
"""The canonical Bot Chat's tip row when *session_id* is in its lineage, else None."""
|
||||
db = authority.db
|
||||
|
||||
@@ -72,6 +72,12 @@ def _free_port():
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
def _get(port, path):
|
||||
req = urllib.request.Request(f'http://127.0.0.1:{port}{path}', headers={'Authorization': f'Bearer {KEY}'})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def _post(port, path, body, timeout=180):
|
||||
req = urllib.request.Request(f'http://127.0.0.1:{port}{path}', data=json.dumps(body).encode(), method='POST',
|
||||
headers={'Authorization': f'Bearer {KEY}', 'Content-Type': 'application/json'})
|
||||
@@ -115,6 +121,8 @@ def overflow_run(tmp_path_factory):
|
||||
assert status == 201, created
|
||||
sid = created['session']['id']
|
||||
first = _post(api_port, f'/api/sessions/{sid}/chat', {'message': big})
|
||||
# ``hermes peer dm`` resolves the Bot Chat by exact title before every DM (``_find_bot_chat``).
|
||||
found = _get(api_port, '/api/sessions?limit=200&title=Bot+Chat&include_hidden=1')['data']
|
||||
follow = _post(api_port, f'/api/sessions/{sid}/chat', {'message': 'short follow-up MKFOLLOW'})
|
||||
finally:
|
||||
model.shutdown()
|
||||
@@ -125,7 +133,7 @@ def overflow_run(tmp_path_factory):
|
||||
with closing(sqlite3.connect(f'file:{home / "state.db"}?mode=ro', uri=True)) as db:
|
||||
rows = db.execute("SELECT session_id, role, substr(coalesce(content,''),1,24) FROM messages ORDER BY id").fetchall()
|
||||
bot_chat = db.execute('SELECT end_reason FROM sessions WHERE id=?', (sid,)).fetchone()
|
||||
return SimpleNamespace(first=first, follow=follow, log=log, rows=rows, rejected=model.rejected,
|
||||
return SimpleNamespace(sid=sid, found=[row['id'] for row in found], first=first, follow=follow, log=log, rows=rows, rejected=model.rejected,
|
||||
bot_chat_end=bot_chat[0])
|
||||
|
||||
|
||||
@@ -141,6 +149,8 @@ def test_peer_dm_overflow_resets_the_local_bot_chat_and_answers_the_follow_up(ov
|
||||
# The overflow is answered with its typed reason, and the Bot Chat was reset after that turn settled.
|
||||
assert run.first[0] == 502 and run.first[1]['error']['code'] == 'context_overflow', run.first
|
||||
assert run.bot_chat_end == 'session_reset', run.rows
|
||||
# The client's title lookup still finds the reset Bot Chat (no title-collision create).
|
||||
assert run.found == [run.sid], run.found
|
||||
# The follow-up runs on the fresh transcript, is answered, and is stored once.
|
||||
assert run.follow[0] == 200, (run.follow, run.rows)
|
||||
assert run.follow[1]['message']['content'].startswith('ACK short follow-up'), run.follow
|
||||
|
||||
Reference in new issue
Block a user