Skip to content

ISessionStore

Namespace AgentPrism · Assembly AgentPrism.Abstractions.dll

The store for serialized agent sessions.

public interface ISessionStore

Session state is the serialized form of Microsoft Agent Framework’s AgentSession object and is treated as opaque. Its content is not interpreted; it is only stored and restored.

Deletes the session.

ValueTask<bool> DeleteAsync(string sessionId, CancellationToken cancellationToken = default)

sessionId string

The session identifier.

cancellationToken CancellationToken

The cancellation token.

ValueTask<bool>

true if the delete happened.

Fetches the session.

ValueTask<SessionRecord?> GetAsync(string sessionId, CancellationToken cancellationToken = default)

sessionId string

The session identifier.

cancellationToken CancellationToken

The cancellation token.

ValueTask<SessionRecord?>

The session; null if it does not exist.

GetOwnerTenantIdAsync(string, CancellationToken)

Section titled “ GetOwnerTenantIdAsync(string, CancellationToken)”

Returns the tenant that owns a session identifier, WITHOUT applying the ambient tenant filter read from ITenantContext.

ValueTask<string?> GetOwnerTenantIdAsync(string sessionId, CancellationToken cancellationToken = default)

sessionId string

The session identifier.

cancellationToken CancellationToken

The cancellation token.

ValueTask<string?>

The identifier of the owning tenant if the identifier has been used; null if the identifier has never been used.

ISessionStore.GetAsync is filtered by the ambient tenant; because of this, it can never answer “does this identifier belong to ANOTHER tenant” — the caller is already inside their own tenant’s context, and another tenant’s record is NEVER VISIBLE from that context, so the result is always null. This is exactly why the OpenAI-compatible endpoints’ cross-tenant ownership check was dead code — the rejection branch never fired, the identifier was silently treated as “never used” and a new session was opened.

The default implementation calls ISessionStore.GetAsync — so it CARRIES THE BUG ABOVE and can never correctly answer the cross-tenant question. This exists only so old/custom stores that have not yet overridden this method keep compiling. The real stores (SqlSessionStore, InMemorySessionStore) override this method with a genuinely correct implementation that is INDEPENDENT of the tenant.

QueryAsync(SessionQuery, CancellationToken)

Section titled “ QueryAsync(SessionQuery, CancellationToken)”

Lists sessions by filter. The most recently updated is returned first.

ValueTask<IReadOnlyList<SessionRecord>> QueryAsync(SessionQuery query, CancellationToken cancellationToken = default)

query SessionQuery

The filter.

cancellationToken CancellationToken

The cancellation token.

ValueTask<IReadOnlyList<SessionRecord>>

The sessions.

SaveAsync(SessionRecord, CancellationToken)

Section titled “ SaveAsync(SessionRecord, CancellationToken)”

Saves the session. Overwrites an existing record with the same identifier.

ValueTask SaveAsync(SessionRecord record, CancellationToken cancellationToken = default)

record SessionRecord

The session to save.

cancellationToken CancellationToken

The cancellation token.

ValueTask

The completion task.

TryCreateAsync(SessionRecord, CancellationToken)

Section titled “ TryCreateAsync(SessionRecord, CancellationToken)”

Creates a new session record only if it does not already exist.

ValueTask<bool> TryCreateAsync(SessionRecord record, CancellationToken cancellationToken = default)

record SessionRecord

The session to create.

cancellationToken CancellationToken

The cancellation token.

ValueTask<bool>

true if created; false if a record with the same identifier already exists.

When two concurrent calls arrive with the same SessionRecord.Id, ONLY one must return true; the loser must get false and read the winner’s record with ISessionStore.GetAsync.

The default implementation is not ATOMIC (check-then-create) — it exists only so old stores that have not yet overridden this method keep compiling. The real stores (SqlSessionStore, InMemorySessionStore) override this method with a genuinely atomic implementation. Without atomicity, two concurrent first requests to the same new session, unaware of each other, generate two different conversation identifiers; the second ISessionStore.SaveAsync unconditionally overwrites the first, and the loser’s messages become silently unreachable.