Navegador

navegador / navegador / cluster / sessions.py
Blame History Raw 151 lines
1
"""
2
SessionManager — branch-isolated session namespacing for agent swarms.
3
4
Each session is tied to a git branch and owns a uniquely named FalkorDB graph
5
so that multiple branches can be ingested and queried concurrently without
6
graph data bleeding across branches.
7
8
Usage:
9
mgr = SessionManager("redis://localhost:6379")
10
11
session_id = mgr.create_session("feature/my-branch", "agent-1")
12
info = mgr.get_session(session_id)
13
graph_name = mgr.session_graph_name(session_id) # e.g. "navegador:sess:abc123"
14
15
for s in mgr.list_sessions():
16
print(s["session_id"], s["branch"])
17
18
mgr.end_session(session_id)
19
"""
20
21
from __future__ import annotations
22
23
import hashlib
24
import json
25
import logging
26
import time
27
import uuid
28
from typing import Any
29
30
logger = logging.getLogger(__name__)
31
32
_SESSIONS_KEY = "navegador:sessions" # Redis hash: session_id -> JSON
33
_SESSION_INDEX_KEY = "navegador:sessions:ids" # Redis set: all session IDs
34
35
36
def _make_session_id() -> str:
37
return str(uuid.uuid4())
38
39
40
def _graph_name_from_session_id(session_id: str) -> str:
41
"""Return a short, deterministic graph name for a session ID."""
42
short = hashlib.sha1(session_id.encode()).hexdigest()[:12]
43
return f"navegador:sess:{short}"
44
45
46
class SessionManager:
47
"""
48
Manage branch-isolated sessions for agent swarms.
49
50
Each session has a unique graph namespace so agents working on different
51
branches do not share graph state.
52
53
Parameters
54
----------
55
redis_url:
56
URL of the Redis server.
57
redis_client:
58
Optional pre-built Redis client (for testing / DI).
59
"""
60
61
def __init__(self, redis_url: str, *, redis_client: Any = None) -> None:
62
self.redis_url = redis_url
63
self._redis = redis_client or self._connect_redis(redis_url)
64
65
# ── Internal ──────────────────────────────────────────────────────────────
66
67
@staticmethod
68
def _connect_redis(url: str) -> Any:
69
try:
70
import redis # type: ignore[import]
71
except ImportError as exc:
72
raise ImportError("Install redis: pip install redis") from exc
73
return redis.from_url(url)
74
75
def _save_session(self, session_id: str, data: dict[str, Any]) -> None:
76
pipe = self._redis.pipeline()
77
pipe.hset(_SESSIONS_KEY, session_id, json.dumps(data))
78
pipe.sadd(_SESSION_INDEX_KEY, session_id)
79
pipe.execute()
80
81
def _load_session(self, session_id: str) -> dict[str, Any] | None:
82
raw = self._redis.hget(_SESSIONS_KEY, session_id)
83
if raw is None:
84
return None
85
text = raw.decode() if isinstance(raw, bytes) else raw
86
return json.loads(text)
87
88
# ── Public API ────────────────────────────────────────────────────────────
89
90
def create_session(self, branch: str, agent_id: str) -> str:
91
"""
92
Create a new isolated session for *agent_id* working on *branch*.
93
94
Returns
95
-------
96
str
97
The new session ID.
98
"""
99
session_id = _make_session_id()
100
data: dict[str, Any] = {
101
"session_id": session_id,
102
"branch": branch,
103
"agent_id": agent_id,
104
"graph_name": _graph_name_from_session_id(session_id),
105
"created_at": time.time(),
106
"status": "active",
107
}
108
self._save_session(session_id, data)
109
logger.info("Created session %s for branch %s / agent %s", session_id, branch, agent_id)
110
return session_id
111
112
def get_session(self, session_id: str) -> dict[str, Any]:
113
"""
114
Retrieve session metadata.
115
116
Raises ``KeyError`` if the session does not exist.
117
"""
118
data = self._load_session(session_id)
119
if data is None:
120
raise KeyError(f"Session not found: {session_id}")
121
return data
122
123
def list_sessions(self) -> list[dict[str, Any]]:
124
"""Return all sessions (active and ended)."""
125
raw_ids = self._redis.smembers(_SESSION_INDEX_KEY)
126
sessions = []
127
for raw_id in raw_ids:
128
sid = raw_id.decode() if isinstance(raw_id, bytes) else raw_id
129
data = self._load_session(sid)
130
if data is not None:
131
sessions.append(data)
132
sessions.sort(key=lambda s: s.get("created_at", 0))
133
return sessions
134
135
def end_session(self, session_id: str) -> None:
136
"""Mark a session as ended (does not delete graph data)."""
137
data = self.get_session(session_id) # raises KeyError if missing
138
data["status"] = "ended"
139
data["ended_at"] = time.time()
140
self._save_session(session_id, data)
141
logger.info("Session %s ended", session_id)
142
143
def session_graph_name(self, session_id: str) -> str:
144
"""
145
Return the namespaced FalkorDB graph name for a session.
146
147
The name is deterministic and safe to use as a FalkorDB graph key.
148
"""
149
data = self.get_session(session_id)
150
return data["graph_name"]
151

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button