Skip to content
Tracon

ISessionStore

Namespace Tracon · Assembly Tracon.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.

DI lifetime — singleton. Registered as a singleton with TryAdd; a consumer’s own registration wins. An implementation must be safe under concurrent calls and must not capture or depend on a scoped service. ISessionStore.TryCreateAsync and ISessionStore.TryUpdateAsync specifically must be genuinely atomic, not check-then-act — see those members’ own remarks. Between them they cover the whole lifetime of a session: the first write and every later one.

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.

TryUpdateAsync(SessionRecord, long, CancellationToken)

Section titled “ TryUpdateAsync(SessionRecord, long, CancellationToken)”

Replaces an EXISTING session record, but only if it has not changed since it was read.

ValueTask<bool> TryUpdateAsync(SessionRecord record, long expectedVersion, CancellationToken cancellationToken = default)

record SessionRecord

The new state of the session.

expectedVersion long

The SessionRecord.Version the caller read. A record read through ISessionStore.GetAsync or ISessionStore.QueryAsync carries it.

cancellationToken CancellationToken

The cancellation token.

ValueTask<bool>

true if the record was replaced; false if it no longer exists or another writer changed it first.

The sibling of ISessionStore.TryCreateAsync, and the reason both exist: ISessionStore.TryCreateAsync makes the first write of a session safe against a concurrent first write; this method makes every LATER write safe against a concurrent later write. ISessionStore.SaveAsync overwrites unconditionally and cannot answer either question.

The write succeeds only when the stored record’s SessionRecord.Version still equals expectedVersion — the value the caller read. On success the stored version is incremented; the caller’s own record is not mutated.

The default implementation is not ATOMIC (read-then-write). It exists only so a store written before this member keep compiling; the real stores (SqlSessionStore, InMemorySessionStore) override it with a genuinely atomic compare-and-swap. Without atomicity two concurrent turns on the SAME EXISTING session both report success and the loser’s turn is silently overwritten — the defect this member was added to close.