-
Notifications
You must be signed in to change notification settings - Fork 10
sessions
Verdict: stateless by default (SessionConfig.enabled=false) — a stateless server keeps no session state at all (SessionStore.noop(), SessionEventStore.noop()). Stateless is a server property, not a session one — no session config ⇒ no sessions. Sessions exist only for 2025-11-25 clients on a stateful server; any session(...) option enables them on its own, Builder#enabled turns them on with defaults, ServerBuilder#stateless writes the opt-out down SessionConfig.Builder#build. Split: process-local Session runtime (connection, cursor, throttle) + immutable SessionSnapshot persisted in pluggable SessionStore with revision CAS → multi-node friendly.
| Type | Role | Proof |
|---|---|---|
Session |
Runtime: id, SessionKey, SessionState, SSE connection, backpressure, cursor, extensions, protocol, log level, resuming stream key |
Session |
SessionState |
INITIALIZING → ACTIVE → (DRAINING) → CLOSED |
SessionState |
SessionKey(sessionId, generationId) |
generation fences stale work; UUID generation per create |
SessionKey, SessionManager#withLifecycleLock
|
SessionSnapshot |
transport-free state + expiresAt + revision
|
SessionSnapshot |
SessionStore |
create/find/compareAndSet/touch/terminate, sync, may do I/O, never on event loop; noop() when sessions off |
SessionStore, NoopSessionStore
|
SessionEventStore |
append/drain/replay event log for SSE resume; noop() when sessions off |
SessionEventStore, NoopSessionEventStore
|
SessionManager |
glue: local map + store + per-id lifecycle lock + janitor | SessionManager |
SessionIdGenerator<T> |
DEFAULT = sess_<uuid-no-dashes>, readsRequest()=false
|
SessionIdGenerator |
-
initialize(no header) →server.createSession(generateSessionId(ctx))on VTMcpDispatcher#dispatchInitializeAsync. Generator gets detached HTTP request copy ifreadsRequest()(ATTR_INIT_REQUEST)McpDispatcher. Blank id ⇒IllegalStateException. -
DefaultDispatchContext.setSessionrecords negotiated protocol on sessionDefaultDispatchContext#setSession. - Response carries
MCP-Session-Id; init handler firesOperationStarted(session)→ bound into channel ctx. -
notifications/initialized→Session.activate()CASINITIALIZING→ACTIVESession#activate. Before that onlypingallowedMcpDispatcher#dispatchTrackedRequestAsync. -
DELETEwith header →removeSession→ 200 / 404McpOperationHandler#handleDelete. - Channel close in init phase →
ShutdownStarted→ removal. - Janitor removes
CLOSEDor idle > TTLSessionManager#sweep.
SessionManager tachyon-core/src/main/java/dev/tachyonmcp/core/server/session/SessionManager.java:
-
createSession→store.create(newKey, expiresAt)under per-idLifecycleLock(ref-countedReentrantLockmap); replaced local session closedSessionManager#createSession,SessionManager. -
getSession→ local map, else hydrate from store (skip + terminate if CLOSED or expired; incompatible protocol version ⇒ empty)SessionManager.getLocalSessionnever hits store (used on hot paths: GET SSE, redelivery). - Mutations (
activate,protocol,enableExtension,loggingLevel,close) callonChange→persist→store.compareAndSet(expected, revision+1); lost CAS ⇒ evict local (another node owns it)SessionManager#persist. -
touch()→onTouch→ async expiry refresh only when withinttl/2ofexpiresAt, deduped per key, on persistence executorSessionManager. - Store choice is resolved once per
build()from the publishedServerConfig, soTachyonServer#configexposes the very stores the server writes toDefaultServerBuilder#build,SessionConfig#sessionStoreOrDefault. ⚠️ Stateless ⇒NoopSessionStore:createmints a snapshot it never keeps,findis always empty, andcompareAndSet/touch/terminateanswertrue— "accepted, nothing to persist". Afalsewould read as lost ownership and evict the local session on its first state changeSessionManager#persist.- In-memory store
InMemorySessionStore—ConcurrentHashMapkeyed by session id. CAS requires same key + higher revisionInMemorySessionStore#compareAndSet.touch/terminateare lock-freeget→replace/removeloops: replacement snapshot built outside the map's bin monitor, lost race re-reads and retriesInMemorySessionStore#touch,InMemorySessionStore#terminate. JMH:tachyon-core/src/test/java/dev/tachyonmcp/core/server/session/InMemorySessionStoreBenchmark.java(make jmh).
Stateless ⇒ NoopSessionEventStore: append discards, drain returns the cursor, so replay is always empty SessionEventStore#noop.
InMemorySessionEventStore tachyon-core/src/main/java/dev/tachyonmcp/core/server/session/InMemorySessionEventStore.java: caps 10 000 total / 512 per session (InMemorySessionEventStore#DEFAULT_MAX_EVENTS), per-session FIFO, global oldest eviction via TreeMap headIndex O(log n), ReentrantLock (VT-safe), snapshot-then-process so slow consumer never blocks append.
SessionEvent sealed SessionEvent: RequestEvent, OutboundRequestEvent, ResponseEvent, NotificationEvent, CancelEvent. Each outbound carries sseEventId + streamKey for per-stream replay → sse-streams.
- Defaults: TTL 30s, janitor 5s
SessionConfig#DEFAULT_SESSION_TTL;SessionConfigcompact ctor rejects session options when disabledSessionConfig#STATELESS. - Liveness bumped by: any request (
session.touch()in dispatcher), any outbound byte (SessionTouchHandler), SSE heartbeat (15s default) — so open GET stream keeps session alive. -
DefaultTachyonServerpassesconfig.runtime().clock()+ executor as persistence executorDefaultTachyonServer#DefaultTachyonServer.
- Notifications:
server.sendNotification(session, …)→ event log append → deliver on bound POST-SSE stream (if dispatching same session) else GET connectionDefaultTachyonServer#sendSerializedNotification. - Requests (elicitation/sampling):
sendRequestregisters pending future withruntime.requestTimeout(60s) and ownershipDefaultTachyonServer. Stateless dispatch ctx refuses: "Server-to-client requests require a session"DefaultDispatchContext#sendRequest. - Broadcasts (
list_changed, logs) iterate ACTIVE local sessions onlyDefaultTachyonServer#broadcastNotification,DefaultTachyonServer#broadcastLog.
WireClientContext#create
encodes an ElicitationRequest record; toElicitationResult validates accepted content before
building the result. Missing/non-object content on ACCEPT fails; absent content on
DECLINE/CANCEL stays null. Value builders: tachyon-api.
Related: sse-streams, configuration, request-lifecycle.
📄 source .llm-wiki/concepts/sessions.md · updated 2026-09-18 · verified at 6a895703 · tags [concept, session, state]
🧭 Start
⚙️ Concepts (cross-cutting)
- request-lifecycle
- netty-pipeline
- protocol-versions
- sessions
- sse-streams
- feature-registries
- tasks
- extensions
- json-layer
- errors
- concurrency
- declarative-configuration
- configuration
- security-guards
- observability
- api-stability
📦 Modules