Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2932,6 +2932,7 @@ def _execute(next_args: dict) -> Any:
around_message_id=next_args.get("around_message_id"),
window=next_args.get("window", 5),
sort=next_args.get("sort"),
scope=next_args.get("scope"),
db=session_db,
current_session_id=agent.session_id,
),
Expand Down
8 changes: 7 additions & 1 deletion agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,13 @@ def _strip_yaml_frontmatter(content: str) -> str:
SESSION_SEARCH_GUIDANCE = (
"When the user references something from a past conversation or you suspect "
"relevant cross-session context exists, use session_search to recall it before "
"asking them to repeat themselves."
"asking them to repeat themselves. Treat session_search as cross-session "
"history lookup, not as the source of truth for ambiguous continuations in "
"the current active session/thread. First re-anchor on the active session/thread "
"when a request is ambiguous. session_search results may include origin and "
"same_origin metadata; same_origin=false means the result belongs to another "
"conversation. In group chats, do not relay cross-chat history as this chat's "
"history without confirmation."
)

SKILLS_GUIDANCE = (
Expand Down
1 change: 1 addition & 0 deletions agent/tool_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1761,6 +1761,7 @@ def _execute(next_args: dict) -> Any:
around_message_id=next_args.get("around_message_id"),
window=next_args.get("window", 5),
sort=next_args.get("sort"),
scope=next_args.get("scope"),
db=session_db,
current_session_id=agent.session_id,
)
Expand Down
1 change: 1 addition & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -20134,6 +20134,7 @@ def run_sync():
provider_require_parameters=pr.get("require_parameters", False),
provider_data_collection=pr.get("data_collection"),
session_id=task_id,
gateway_session_key=self._session_key_for_source(source),
platform=platform_key,
user_id=source.user_id,
user_id_alt=source.user_id_alt,
Expand Down
42 changes: 28 additions & 14 deletions hermes_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -6751,33 +6751,41 @@ def get_session_by_title(self, title: str) -> Optional[Dict[str, Any]]:
row = cursor.fetchone()
return self._session_row_dict(row) if row else None

def resolve_session_by_title(self, title: str) -> Optional[str]:
def resolve_session_by_title(self, title: str, session_key: str = None, chat_id: str = None) -> Optional[str]:
"""Resolve a title to a session ID, preferring the latest in a lineage.

If the exact title exists, returns that session's ID.
If not, searches for "title #N" variants and returns the latest one.
If the exact title exists AND numbered variants exist, returns the
latest numbered variant (the most recent continuation).
latest numbered variant (the most recent continuation). When
``session_key`` is provided, restrict resolution to that gateway-origin
boundary so a same-titled session in another chat cannot shadow the
current chat's match. When only ``chat_id`` is available, restrict to
that legacy chat boundary.
"""
# First try exact match
exact = self.get_session_by_title(title)

# Also search for numbered variants: "title #2", "title #3", etc.
# Escape SQL LIKE wildcards (%, _) in the title to prevent false matches
escaped = _escape_like(title)
where = "WHERE (title = ? OR title LIKE ? ESCAPE '\\')"
params: list = [title, f"{escaped} #%"]
if session_key:
where += " AND session_key = ?"
params.append(session_key)
elif chat_id:
where += " AND chat_id = ?"
params.append(chat_id)

with self._read_ctx() as conn:
cursor = conn.execute(
"SELECT id, title, started_at FROM sessions "
"WHERE title LIKE ? ESCAPE '\\' ORDER BY started_at DESC",
(f"{escaped} #%",),
f"{where} ORDER BY CASE WHEN title = ? THEN 1 ELSE 0 END, started_at DESC",
(*params, title),
)
numbered = cursor.fetchall()
rows = cursor.fetchall()

numbered = [row for row in rows if row["title"] != title]
if numbered:
# Return the most recent numbered variant
return numbered[0]["id"]
elif exact:
return exact["id"]
if rows:
return rows[0]["id"]
return None

def get_next_title_in_lineage(self, base_title: str) -> str:
Expand Down Expand Up @@ -6890,6 +6898,7 @@ def list_sessions_rich(
self,
source: str = None,
sources: List[str] = None,
chat_id: str = None,
exclude_sources: List[str] = None,
cwd_prefix: str = None,
limit: int = 20,
Expand Down Expand Up @@ -6956,7 +6965,9 @@ def list_sessions_rich(

Pass ``session_key`` to restrict results to one stable gateway
conversation scope (DM, group, channel, or thread, including the
configured per-user isolation policy).
configured per-user isolation policy). Pass ``chat_id`` to restrict
results to one platform chat while preserving the broader source and
session-key filters.
"""
# Rows carry token/cost totals — drain queued deltas first so
# listings (sidebar, /resume, dashboards) show exact counters.
Expand Down Expand Up @@ -6990,6 +7001,9 @@ def list_sessions_rich(
if session_key:
where_clauses.append("s.session_key = ?")
params.append(session_key)
if chat_id:
where_clauses.append("s.chat_id = ?")
params.append(chat_id)
if exclude_sources:
placeholders = ",".join("?" for _ in exclude_sources)
where_clauses.append(f"s.source NOT IN ({placeholders})")
Expand Down
99 changes: 92 additions & 7 deletions hermes_state_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ class SessionSearchMixin:
"source",
"model",
"session_started",
"session_key",
"chat_id",
"chat_type",
"display_name",
"context",
)

Expand Down Expand Up @@ -1338,6 +1342,8 @@ def _run_trigram_search(
source_filter: List[str] = None,
exclude_sources: List[str] = None,
role_filter: List[str] = None,
chat_id: str = None,
session_key: str = None,
limit: int = 20,
offset: int = 0,
) -> Optional[List[Dict[str, Any]]]:
Expand Down Expand Up @@ -1379,6 +1385,12 @@ def _run_trigram_search(
if role_filter:
tri_where.append(f"m.role IN ({','.join('?' for _ in role_filter)})")
tri_params.extend(role_filter)
if session_key:
tri_where.append("s.session_key = ?")
tri_params.append(session_key)
elif chat_id:
tri_where.append("s.chat_id = ?")
tri_params.append(chat_id)
tri_sql = f"""
SELECT
m.id,
Expand All @@ -1390,7 +1402,11 @@ def _run_trigram_search(
m.tool_name,
s.source,
s.model,
s.started_at AS session_started
s.started_at AS session_started,
s.session_key,
s.chat_id,
s.chat_type,
s.display_name
FROM {table}
JOIN messages m ON m.id = {table}.rowid
JOIN sessions s ON s.id = m.session_id
Expand All @@ -1413,6 +1429,8 @@ def search_messages(
source_filter: List[str] = None,
exclude_sources: List[str] = None,
role_filter: List[str] = None,
chat_id: str = None,
session_key: str = None,
limit: int = 20,
offset: int = 0,
sort: str = None,
Expand All @@ -1435,6 +1453,8 @@ def search_messages(
source_filter=source_filter,
exclude_sources=exclude_sources,
role_filter=role_filter,
chat_id=chat_id,
session_key=session_key,
limit=limit,
offset=offset,
sort=sort,
Expand Down Expand Up @@ -1545,6 +1565,8 @@ def _search_messages_like_fallback(
source_filter: Optional[List[str]],
exclude_sources: Optional[List[str]],
role_filter: Optional[List[str]],
chat_id: str = None,
session_key: str = None,
limit: int,
offset: int,
sort: Optional[str],
Expand All @@ -1569,6 +1591,12 @@ def _search_messages_like_fallback(
if role_filter:
where.append(f"m.role IN ({','.join('?' for _ in role_filter)})")
params.extend(role_filter)
if session_key:
where.append("s.session_key = ?")
params.append(session_key)
elif chat_id:
where.append("s.chat_id = ?")
params.append(chat_id)

order = (
"ASC"
Expand All @@ -1579,7 +1607,8 @@ def _search_messages_like_fallback(
SELECT m.id, m.session_id, m.role,
substr(m.content, max(1, instr(m.content, ?) - 40), 120) AS snippet,
m.content, m.timestamp, m.tool_name,
s.source, s.model, s.started_at AS session_started
s.source, s.model, s.started_at AS session_started,
s.session_key, s.chat_id, s.chat_type, s.display_name
FROM messages m
JOIN sessions s ON s.id = m.session_id
WHERE {' AND '.join(where)}
Expand Down Expand Up @@ -1702,6 +1731,8 @@ def _search_messages_impl(
source_filter: List[str] = None,
exclude_sources: List[str] = None,
role_filter: List[str] = None,
chat_id: str = None,
session_key: str = None,
limit: int = 20,
offset: int = 0,
sort: str = None,
Expand Down Expand Up @@ -1755,6 +1786,8 @@ def _search_messages_impl(
source_filter=source_filter,
exclude_sources=exclude_sources,
role_filter=role_filter,
chat_id=chat_id,
session_key=session_key,
limit=limit,
offset=offset,
sort=sort,
Expand Down Expand Up @@ -1808,6 +1841,12 @@ def _search_messages_impl(
role_placeholders = ",".join("?" for _ in role_filter)
where_clauses.append(f"m.role IN ({role_placeholders})")
params.extend(role_filter)
if session_key:
where_clauses.append("s.session_key = ?")
params.append(session_key)
elif chat_id:
where_clauses.append("s.chat_id = ?")
params.append(chat_id)

where_sql = " AND ".join(where_clauses)
params.extend([limit, offset])
Expand All @@ -1823,7 +1862,11 @@ def _search_messages_impl(
m.tool_name,
s.source,
s.model,
s.started_at AS session_started
s.started_at AS session_started,
s.session_key,
s.chat_id,
s.chat_type,
s.display_name
FROM messages_fts
JOIN messages m ON m.id = messages_fts.rowid
JOIN sessions s ON s.id = m.session_id
Expand Down Expand Up @@ -1902,6 +1945,12 @@ def _search_messages_impl(
if role_filter:
cjk_where.append(f"m.role IN ({','.join('?' for _ in role_filter)})")
cjk_params.extend(role_filter)
if session_key:
cjk_where.append("s.session_key = ?")
cjk_params.append(session_key)
elif chat_id:
cjk_where.append("s.chat_id = ?")
cjk_params.append(chat_id)
cjk_sql = f"""
SELECT
m.id,
Expand All @@ -1913,7 +1962,11 @@ def _search_messages_impl(
m.tool_name,
s.source,
s.model,
s.started_at AS session_started
s.started_at AS session_started,
s.session_key,
s.chat_id,
s.chat_type,
s.display_name
FROM messages_fts_cjk
JOIN messages m ON m.id = messages_fts_cjk.rowid
JOIN sessions s ON s.id = m.session_id
Expand Down Expand Up @@ -1991,6 +2044,12 @@ def _search_messages_impl(
if role_filter:
tri_where.append(f"m.role IN ({','.join('?' for _ in role_filter)})")
tri_params.extend(role_filter)
if session_key:
tri_where.append("s.session_key = ?")
tri_params.append(session_key)
elif chat_id:
tri_where.append("s.chat_id = ?")
tri_params.append(chat_id)
tri_sql = f"""
SELECT
m.id,
Expand All @@ -2002,7 +2061,11 @@ def _search_messages_impl(
m.tool_name,
s.source,
s.model,
s.started_at AS session_started
s.started_at AS session_started,
s.session_key,
s.chat_id,
s.chat_type,
s.display_name
FROM messages_fts_trigram
JOIN messages m ON m.id = messages_fts_trigram.rowid
JOIN sessions s ON s.id = m.session_id
Expand Down Expand Up @@ -2085,13 +2148,20 @@ def _search_messages_impl(
if role_filter:
like_where.append(f"m.role IN ({','.join('?' for _ in role_filter)})")
like_params.extend(role_filter)
if session_key:
like_where.append("s.session_key = ?")
like_params.append(session_key)
elif chat_id:
like_where.append("s.chat_id = ?")
like_params.append(chat_id)
like_sql = f"""
SELECT m.id, m.session_id, m.role,
substr(m.content,
max(1, instr(m.content, ?) - 40),
120) AS snippet,
m.content, m.timestamp, m.tool_name,
s.source, s.model, s.started_at AS session_started
s.source, s.model, s.started_at AS session_started,
s.session_key, s.chat_id, s.chat_type, s.display_name
FROM messages m
JOIN sessions s ON s.id = m.session_id
WHERE {' AND '.join(like_where)}
Expand Down Expand Up @@ -2144,6 +2214,8 @@ def _search_messages_impl(
source_filter=source_filter,
exclude_sources=exclude_sources,
role_filter=role_filter,
chat_id=chat_id,
session_key=session_key,
)
seen_ids = {m["id"] for m in matches}
matches.extend(m for m in gap_matches if m["id"] not in seen_ids)
Expand Down Expand Up @@ -2182,6 +2254,8 @@ def _search_messages_impl(
source_filter=source_filter,
exclude_sources=exclude_sources,
role_filter=role_filter,
chat_id=chat_id,
session_key=session_key,
limit=limit,
offset=offset,
)
Expand All @@ -2199,6 +2273,8 @@ def _search_messages_impl(
source_filter=source_filter,
exclude_sources=exclude_sources,
role_filter=role_filter,
chat_id=chat_id,
session_key=session_key,
limit=limit,
offset=offset,
)
Expand All @@ -2216,6 +2292,8 @@ def _search_unindexed_gap(
source_filter: Optional[List[str]] = None,
exclude_sources: Optional[List[str]] = None,
role_filter: Optional[List[str]] = None,
chat_id: str = None,
session_key: str = None,
) -> List[Dict[str, Any]]:
"""LIKE-scan the rows the deferred rebuild hasn't indexed yet.

Expand Down Expand Up @@ -2261,14 +2339,21 @@ def _search_unindexed_gap(
if role_filter:
where.append(f"m.role IN ({','.join('?' for _ in role_filter)})")
params.extend(role_filter)
if session_key:
where.append("s.session_key = ?")
params.append(session_key)
elif chat_id:
where.append("s.chat_id = ?")
params.append(chat_id)

sql = f"""
SELECT m.id, m.session_id, m.role,
substr(m.content,
max(1, instr(m.content, ?) - 40),
120) AS snippet,
m.content, m.timestamp, m.tool_name,
s.source, s.model, s.started_at AS session_started
s.source, s.model, s.started_at AS session_started,
s.session_key, s.chat_id, s.chat_type, s.display_name
FROM messages m
JOIN sessions s ON s.id = m.session_id
WHERE {' AND '.join(where)}
Expand Down
Loading
Loading