refactor(gateway): add SessionStore.peek_session_id public accessor for webhook close

Replace the webhook delivery-close path's direct reach into private
SessionStore._entries (which also bypassed the store lock) with a public,
lock-held peek_session_id(session_key) accessor. Mirrors the existing
lookup_by_session_id inverse helper. Keeps a getattr fallback for older
stores / test doubles. Adds a unit test for the accessor.
This commit is contained in:
kshitijk4poor 2026-07-03 03:17:07 +05:30 committed by kshitij
parent de67f430b2
commit 65cb70b8d0
3 changed files with 71 additions and 8 deletions

View file

@ -776,14 +776,23 @@ class WebhookAdapter(BasePlatformAdapter):
if key_fn is None:
return
session_key = key_fn(event.source)
if hasattr(store, "_ensure_loaded"):
try:
store._ensure_loaded()
except Exception:
pass
entries = getattr(store, "_entries", {}) or {}
entry = entries.get(session_key)
session_id = getattr(entry, "session_id", None) if entry else None
# Resolve the persisted session_id via the store's public,
# lock-held accessor (peek_session_id) rather than reaching into
# the private _entries dict without the store lock. Fall back to
# the private path only for older stores / test doubles that
# predate the accessor.
peek = getattr(store, "peek_session_id", None)
if callable(peek):
session_id = peek(session_key)
else:
if hasattr(store, "_ensure_loaded"):
try:
store._ensure_loaded()
except Exception:
pass
entries = getattr(store, "_entries", {}) or {}
entry = entries.get(session_key)
session_id = getattr(entry, "session_id", None) if entry else None
if not session_id:
logger.debug(
"[webhook] No session_id to close for %s (key=%s)",

View file

@ -1887,6 +1887,22 @@ class SessionStore:
if entry.session_id == session_id:
return entry
return None
def peek_session_id(self, session_key: str) -> Optional[str]:
"""Return the persisted session_id currently bound to a session key.
Public, lock-held accessor for the keysession_id mapping. Callers that
need to resolve the session row for a source (e.g. the webhook
delivery-close path) should use this rather than reaching into the
private ``_entries`` dict without holding ``self._lock``. Returns None
when the key is unknown or has no session_id yet.
"""
if not session_key:
return None
with self._lock:
self._ensure_loaded_locked()
entry = self._entries.get(session_key)
return getattr(entry, "session_id", None) if entry else None
def append_to_transcript(self, session_id: str, message: Dict[str, Any], skip_db: bool = False) -> None:
"""Append a message to a session's transcript (SQLite).

View file

@ -176,3 +176,41 @@ async def test_webhook_session_closed_even_when_agent_run_raises(tmp_path):
)
assert row["end_reason"] == "webhook_complete"
store._db.close()
def test_peek_session_id_resolves_bound_key(tmp_path):
"""SessionStore.peek_session_id returns the session_id bound to a key.
This is the public, lock-held accessor the webhook close path uses to
resolve a session row from its key without reaching into the private
``_entries`` dict. A missing/unknown key returns None (so the close path
debug-logs and no-ops rather than closing the wrong row).
"""
sessions_dir = tmp_path / "sessions"
sessions_dir.mkdir()
config = GatewayConfig(
platforms={Platform.WEBHOOK: PlatformConfig(enabled=True)}
)
store = SessionStore(sessions_dir=sessions_dir, config=config)
adapter = _make_adapter(
{"alerts": {"secret": _INSECURE_NO_AUTH, "prompt": "x", "deliver": "log"}}
)
source = adapter.build_source(
chat_id="webhook:alerts:peek-001",
chat_name="webhook/alerts",
chat_type="webhook",
user_id="webhook:alerts",
user_name="alerts",
)
entry = store.get_or_create_session(source)
key = store._generate_session_key(source)
# Known key → the bound session_id.
assert store.peek_session_id(key) == entry.session_id
# Unknown key and empty key → None (never a wrong-row close).
assert store.peek_session_id("no:such:key") is None
assert store.peek_session_id("") is None
if store._db is not None:
store._db.close()