Table of Contents

Conversation Persistence

Last updated:

Use IChatThreadRepository as the conversation persistence boundary. Its file-backed ChatThreadRepository implementation stores thread summaries, display transcripts, and serialized framework session state beneath a users root selected by your host.

The repository belongs to the reusable Oscova.Agents package. Your host remains responsible for user identity, authorization, path policy, dependency lifetimes, and choosing the users root.

Construct the repository

Supply a root resolver, one shared FileSessionIndexCoordinator, an IRedactionService, an IContentProtector, an agent-definition repository, and a logger. The resolver is evaluated when an operation derives a path, so a host can change its active root without rebuilding the repository.

using Microsoft.Extensions.Logging;
using Oscova.Agents.Conversations;
using Oscova.Agents.Definitions;
using Oscova.Agents.Persistence;
using Oscova.Agents.Secrets;

var repository = new ChatThreadRepository(
    () => usersRoot,
    new FileSessionIndexCoordinator(),
    redactionService,
    contentProtector,
    agentDefinitions,
    logger);

Use one coordinator for repositories that can update the same users root. The redaction service masks persisted instructions, message text, tool arguments, and tool results according to host policy. The protector encrypts content for agents whose definitions are confidential. The host owns the key lifecycle behind that interface.

Use the conversation contract

IChatThreadRepository supports these operations:

  • list, load, save, and delete ChatThread records.
  • save, load, and delete serialized framework session state.
  • mark or clear the current thread for an agent.
  • update indexing, summary, and reflection watermarks.
  • delete all threads for a user or agent.

Pass the caller's cancellation token to repository methods. Asynchronous mutations pass it through index coordination, redaction, and file writes. Read-only methods complete from local file reads and expose the token for interface consistency.

Coordinate conversation runs

Use ThreadRunGuard when every transport must share one active run for an exact user and case-insensitive thread identity. Acquire admits immediately or returns null, while AcquireAsync waits until the current run or user-wide block releases. Dispose the returned run lease to release the slot.

Cancel stops the current run, CancelExact stops only the matching run, and CancelOrDefer preserves an exact cancellation that arrives before admission. CancelAndAcquireUserAsync cancels and drains all active runs for one user, then blocks new admissions until its user lease is disposed.

using Oscova.Agents.Conversations;

using var lease = runGuard.Acquire(userId, threadId, runId);
if (lease is null)
{
    // The host decides whether to reject the request or wait with AcquireAsync.
    return;
}

await agent.RunAsync(message, session, cancellationToken: lease.CancellationToken);

Your host remains responsible for authorization, user identity, transport behavior, service lifetime, and deciding whether a competing run is rejected or waits.

Understand the disk layout

For a users root such as /app/users, the repository uses this stable layout:

<users root>/
  <user id>/
    chat/
      index.jsonl
      <thread id>.jsonl
      <thread id>.session.json

index.jsonl contains thread summaries. Each <thread id>.jsonl file contains the display transcript, while the optional .session.json sidecar contains serialized framework session state.

Confidential threads carry an encrypted state on their index row and transcript records. Their title, rendered system instruction, messages, tool content, and session sidecar are protected. GetThreadsAsync(userId, includeEncrypted: false) filters them before projection, so background indexing and memory work does not need to open protected titles.

Persist channel values with ChatChannels: web_chat, discord_chat, and telegram_chat. These names are stable disk values rather than product UI labels.

Own identity and path policy in the host

The repository confines transcript and session sidecars to the selected user's chat directory, but it does not authenticate callers or define which user identifiers your product accepts. Authorize each operation and apply your product's user-ID and root-selection policy before calling the repository.

A null root resolver fails at construction. Invalid thread paths, file-system failures, redaction failures, protection failures, and cancellation propagate to the caller. Malformed index rows can be skipped where the repository can continue safely. Corrupt encrypted transcript or session payloads fail the read instead of returning partial content. Use the supplied logger to diagnose those records.