OptionalrealtimeClientSessionService: RealtimeClientSessionServiceThe shared RealtimeClientSessionService instance to
finalize observability runs through. Pass the SAME instance your resolver/service used to create
and write to the session's co-agent AIAgentRun/AIPromptRun (e.g.
RealtimeClientSessionResolver's own field) so CloseSession can correctly clean up that
instance's per-run state. Omit only when this SessionManager never closes a client-direct
voice session it also originated observability writes for.
Close a session (terminal). Sets Status = 'Closed' + ClosedAt = now + CloseReason, then
disconnects all of its channel rows. Idempotent: closing an already-Closed session is a
no-op that returns true (and never overwrites the original CloseReason). A
missing/unloadable session returns false.
Why the session is being closed. Defaults to 'Explicit' so existing
call sites (the user-initiated close mutations) stamp the common case without modification;
background callers (janitor sweeps, shutdown drain, error teardowns) must pass their reason
explicitly. NULL rows remain only for legacy data closed before the column existed.
OptionalpreloadedChannels: MJAIAgentSessionChannelEntity[]Closes a session that was created for one bridged agent (a bot in a meeting room) once that agent's bridge has
ended, whatever ended it: the agent was stopped, the meeting was ended, everyone left, the model session was lost.
Without this the session stays Active until the janitor's staleness sweep closes it, 15 minutes or more later.
The close is CloseSession, the one the janitor runs, with the bridge's reason: Explicit for a stop, an
ended meeting or an emptied room (HostEnded), otherwise the bridge's own (Error, Janitor, Shutdown). A
session already closed (by the janitor, or by its user) keeps its reason.
Never throws: it runs as the bridge's end-of-session bookkeeping. A failure is logged and leaves the session to the janitor, as before.
The session created for the bridged agent.
Why the bridge ended.
The user the session was created for.
The metadata provider the session was created through.
true when the session is (or already was) closed.
Closes a session that was created for one agent's time in a room when that agent failed to start: its model session
could not open, the bot's token could not be minted, the bridge did not start. The agent never joined, so no
end-of-session hook will close the session; without this it stays Active until the janitor's staleness sweep.
The close is CloseSession with reason Error. A session already closed keeps its reason.
Never throws: it runs while the caller handles the failed start, whose error is the one the caller reports. A failure is logged and leaves the session to the janitor.
The session created for the agent.
The user the session was created for.
The metadata provider the session was created through.
true when the session is (or already was) closed.
Authorize and create a new session. Flow:
CanRun check on the agent — denied → SessionAuthorizationError, no row written.AIAgentSession as Active, stamped with this host and LastActiveAt = now.the saved MJAIAgentSessionEntity.
Record activity on a session. Coalesces persisted writes so at most one LastActiveAt update
lands per session per HEARTBEAT_MIN_WRITE_INTERVAL_MS; intervening calls return true
without touching the DB. Reactivates an Idle session to Active. A Closed session is not
reactivated (returns false).
Batch-loads active channels for many sessions at once and groups them by AgentSessionID
(lowercased), so a sweep can disconnect a whole page of sessions with ONE query instead of
one-per-session. Sessions with no active channels are absent from the map (callers default
to []). Pass each session's slice to CloseSession's preloadedChannels.
Transition an Active session to Idle (e.g. when its last channel disconnects). No-op when
the session is already Idle or Closed. Returns true on success / no-op-needed.
Server-side lifecycle manager for
AIAgentSessionrecords (not the media/socket transport — that arrives in P5). Stateless with respect to the caller: every method takes the request'scontextUserand request-scopedIMetadataProviderso all entity operations run under the correct identity/provider and never pin the process-global provider.The only in-memory state it holds is a small per-session last-heartbeat-write timestamp cache used purely to coalesce Heartbeat writes (see heartbeatLastWrite for why it's bounded), plus the injected RealtimeClientSessionService used by finalizeObservabilityRuns; it carries no user or provider state across calls.
Responsibilities:
CanRun), optionally mint a Conversation, persist anActivesession.LastActiveAtbump; reactivatesIdle → Active.Active → Idletransition when the last channel goes quiet.Remarks
P5 will extend CloseSession to also abort any in-flight
AIAgentRun(via its cancellation token) and callClientToolRequestManager.ClearSession(...); those resources don't exist yet at this layer, so close here only reconciles the durable record + channel rows.