fix(session): persist profile_name and route batch key by profile

Two follow-ups observed after deploying profile routing:

1. sessions.profile_name was NULL even when the agent ran inside the
   routed profile scope. _insert_session_row never wrote it,
   get_or_create_session / reset_session never passed it through, and
   the agent-side _ensure_db_session fallback had no way to read it.
   - Declare profile_name TEXT in SCHEMA_SQL so _reconcile_columns
     auto-adds it on existing DBs.
   - _insert_session_row takes profile_name and writes it.
   - SessionStore passes source.profile (or old_entry.origin.profile
     on reset) into db_create_kwargs.
   - _ensure_db_session reads the active profile via
     get_active_profile_name() inside _profile_runtime_scope.

2. DiscordAdapter._text_batch_key called build_session_key without
   profile=, so the batch key always landed in agent:main even when
   the routed profile differed — diverging from the agent session
   key namespace (agent:crypto-trader, agent:ai-expert, ...).
   Pass event.source.profile through so both namespaces agree.

Live verification (jth-server-2, 2026-06-28): a test message in a
routed #coin thread produced agent:crypto-trader:discord🧵...
in the batch log and profile_name=crypto-trader in the sessions
row. Default-routed chat still produced agent:main / NULL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Burgunthy 2026-06-28 05:51:49 +09:00 committed by Teknium
parent d7993ab178
commit e8b7ce8c19
4 changed files with 26 additions and 4 deletions

View file

@ -1993,6 +1993,7 @@ class SessionStore:
"chat_id": source.chat_id,
"chat_type": source.chat_type,
"thread_id": source.thread_id,
"profile_name": source.profile,
}
if _needs_save:
@ -2273,6 +2274,7 @@ class SessionStore:
"chat_id": old_entry.origin.chat_id if old_entry.origin else None,
"chat_type": old_entry.origin.chat_type if old_entry.origin else None,
"thread_id": old_entry.origin.thread_id if old_entry.origin else None,
"profile_name": old_entry.origin.profile if old_entry.origin else None,
}
if self._db and db_end_session_id:

View file

@ -791,6 +791,7 @@ CREATE TABLE IF NOT EXISTS sessions (
compression_failure_cooldown_until REAL,
compression_failure_error TEXT,
compression_fallback_streak INTEGER NOT NULL DEFAULT 0,
profile_name TEXT,
rewind_count INTEGER NOT NULL DEFAULT 0,
archived INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
@ -1759,6 +1760,7 @@ class SessionDB:
thread_id: str = None,
parent_session_id: str = None,
cwd: str = None,
profile_name: str = None,
) -> None:
"""Insert a session row, enriching NULL metadata on conflict.
@ -1782,9 +1784,9 @@ class SessionDB:
conn.execute(
"""INSERT INTO sessions (
id, source, user_id, session_key, chat_id, chat_type, thread_id,
model, model_config, system_prompt, parent_session_id, cwd, started_at
model, model_config, system_prompt, parent_session_id, cwd, profile_name, started_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
model = COALESCE(sessions.model, excluded.model),
model_config = COALESCE(sessions.model_config, excluded.model_config),
@ -1794,7 +1796,8 @@ class SessionDB:
chat_type = COALESCE(sessions.chat_type, excluded.chat_type),
thread_id = COALESCE(sessions.thread_id, excluded.thread_id),
parent_session_id = COALESCE(sessions.parent_session_id, excluded.parent_session_id),
cwd = COALESCE(sessions.cwd, excluded.cwd)""",
cwd = COALESCE(sessions.cwd, excluded.cwd),
profile_name = COALESCE(sessions.profile_name, excluded.profile_name)""",
(
session_id,
source,
@ -1808,6 +1811,7 @@ class SessionDB:
system_prompt,
parent_session_id,
cwd,
profile_name,
time.time(),
),
)

View file

@ -6612,12 +6612,20 @@ class DiscordAdapter(BasePlatformAdapter):
# ------------------------------------------------------------------
def _text_batch_key(self, event: MessageEvent) -> str:
"""Session-scoped key for text message batching."""
"""Session-scoped key for text message batching.
Passes ``event.source.profile`` through so routed messages batch
under the same namespace the agent run will use (e.g.
``agent:crypto-trader`` instead of ``agent:main``). Without this,
the batch key would always land in ``agent:main`` even when the
routed profile differs.
"""
from gateway.session import build_session_key
return build_session_key(
event.source,
group_sessions_per_user=self.config.extra.get("group_sessions_per_user", True),
thread_sessions_per_user=self.config.extra.get("thread_sessions_per_user", False),
profile=event.source.profile,
)
def _enqueue_text_event(self, event: MessageEvent) -> None:

View file

@ -599,6 +599,13 @@ class AIAgent:
return
source = _session_source_for_agent(self.platform)
try:
try:
from hermes_cli.profiles import get_active_profile_name
_profile_for_session = get_active_profile_name()
if _profile_for_session == "default":
_profile_for_session = None
except Exception:
_profile_for_session = None
self._session_db.create_session(
session_id=self.session_id,
source=source,
@ -608,6 +615,7 @@ class AIAgent:
user_id=None,
parent_session_id=self._parent_session_id,
cwd=_launch_cwd_for_session(source),
profile_name=_profile_for_session,
)
self._session_db_created = True
except Exception as e: