Skip to content
Tracon

Persistence

Without a database every store is in memory and everything ends with the process. That is deliberate — it makes the first agent work with no infrastructure — but it is not where you stop.

builder.AddTracon()
.UsePostgreSql(connectionString); // Tracon.PostgreSql
Package Choose it when
Tracon.PostgreSql The default. The only one with vector search for knowledge
Tracon.SqlServer You already run SQL Server
Tracon.Sqlite One node, or a durable local development setup

All three implement the same store contracts and pass the same shared contract tests. Their operational limits differ: only PostgreSQL supports Knowledge, only PostgreSQL keeps the AOT promise, and SQLite is a single-node choice.

Binding from configuration is the usual shape:

.UsePostgreSql(builder.Configuration.GetSection(TraconPostgreSqlOptions.SectionName))
appsettings.json
{
"Tracon": {
"PostgreSql": {
"ConnectionString": "",
"SchemaName": "tracon",
"AutoApplyMigrations": true,
"CommandTimeoutSeconds": 30,
"EnableKnowledge": false,
"EnableReadViews": false
}
}
}
Provider Namespace Migration coordination
PostgreSQL Separate tracon schema by default pg_advisory_lock, scoped to the schema
SQL Server Separate tracon schema by default; your dbo objects stay untouched sp_getapplock, scoped to the schema
SQLite No schema support; tracon_ table prefix by default A sidecar file lock next to the database

Rename SchemaName or TablePrefix when your conventions require it. A bare SQLite Data Source=:memory: connection is rejected because each opened connection would see a different database; use a shared in-memory URI for tests.

The SQL files ship embedded in the assembly and are applied when the application starts, unless that responsibility is moved to its own deployment step:

flowchart TD
    accTitle: Two ways a migration is applied
    accDescr: With AutoApplyMigrations true, the application applies pending migrations itself at startup. With it false, a separate tracon migrate step applies the schema first, and the application only verifies it before starting.
    START["Application starts"] --> CHECK{"AutoApplyMigrations"}
    CHECK -->|"true (default)"| APPLY["Applies pending migrations itself<br/>provider lock serializes concurrent instances"]
    CHECK -->|"false"| SEPARATE["tracon migrate<br/>runs as its own deployment step, no app needed"]
    SEPARATE --> APP2["Application starts, verifies the schema, does not write"]
    APPLY --> READY["Ready"]
    APP2 --> READY

Two properties make in-process application safe with several instances starting at once:

  • The runner takes the provider-specific lock shown above, so instances serialize instead of racing.
  • Each applied file’s SHA-256 is recorded. If the content later differs from what was applied, startup fails loudly rather than running against a schema that is not what the code expects.

Set AutoApplyMigrations = false when schema changes are their own deployment step. Tracon then verifies but does not write. The diagnostics endpoint can report whether the schema is current, but it is deliberately not mapped by default because it exposes setup details:

app.MapTracon("/tracon", options =>
{
options.EnableDiagnosticsEndpoint = true;
});

After that opt-in, GET /tracon/api/diagnostics is an Admin surface and still passes through the configured access layers.

Something still needs to apply the schema before the application starts with AutoApplyMigrations = false. The tracon CLI does that as its own step, against the database directly — no running application required:

Terminal window
tracon migrate --provider postgres --connection "$TRACON_CONNECTION"

Running it again applies nothing (0 applied) and exits 0; tracon migrate status lists pending migration names without writing. See the typed client and CLI guide for setup and the rest of the commands.

Giving your own data source instead of a connection string

Section titled “Giving your own data source instead of a connection string”

Each provider’s Options.DataSource field accepts a DbDataSource you built yourself instead of ConnectionString — most useful when your host already owns one, for example an EF Core DbContext configured with an NpgsqlDataSource:

.UsePostgreSql(options => options.DataSource = yourDataSource)

Tracon never disposes an instance it did not build; ownership stays with whoever created it. Building two separate data sources from the identical connection string does not share a connection pool — see Two data planes, one connection pool or two and, for the full EF Core pattern, Two connection planes: EF Core and Tracon.

Runs, events, and tool calls survive restarts, so the console shows real history rather than the current process. Sessions can be read back as chat history rather than an opaque blob — which is also what makes branching a conversation possible. Queued runs, schedules, evals, experiments, quotas, and the pending-approval mailbox all become usable, since they depend on state outliving a request. A resolved approval presentation is persisted next to the raw call arguments, so it survives a restart the same way the arguments do.

It is also what makes recovering from a crash possible at all: a run’s recorded tool calls are what an automatically continued run replays instead of repeating, and orphan reconciliation itself only has a stale Running row to find because that row, and every tool call it already made, outlived the process that opened it.

Session and workflow checkpoint rows carry a version stamp of their own, separate from the schema migrations above: Tracon’s envelope around the row, and the Microsoft Agent Framework version that wrote the opaque state inside it. See Versions and upgrades for what that stamp promises and what happens when an old row can no longer be read.

Durable state is also state an upgrade has to keep being able to read, and the stamp is what makes that answerable ahead of time rather than in production:

Terminal window
tracon state-check --provider postgres --connection "$TRACON_CONNECTION"

Run with the new version of the tool, it counts your stored rows by stamp, says which of them the new build understands, and decodes a sample of each. It writes nothing, so it is safe against the live database. See the supported upgrade window.

A recorded run is data, and recorded runs accumulate. Retention policies set an age or row limit per target — run events, tool calls, traces, jobs, webhook deliveries, eval results, checkpoints, and more.

Database policies take precedence. When no database policy exists and Tracon:Retention:Enabled is true, configuration falls back to built-in target defaults, such as 30 days for run events and 14 days for spans. With retention disabled, nothing is deleted. An archive: true policy also deletes nothing when no IArchiveSink is registered; data loss is the failure mode the worker avoids.

Cleanup runs through the job queue. Preview a policy before you execute it:

Terminal window
curl 'http://localhost:5081/tracon/api/retention/preview'
curl -X POST 'http://localhost:5081/tracon/api/retention/run'

A durable audit_log can be verified: GET /api/audit/verify walks a hash chain and reports whether any entry was altered or deleted after it was written. And because sessions, runs, and conversations are real rows now, a data subject’s content can be found and erased by identity, not just aged out — see Data subject rights.

Both endpoints accept an optional tenantId filter; leaving it out never means “every tenant” — it resolves to the caller’s own ambient tenant. A custom IAuditLog implementation must apply this same fallback (see Write your own store).

Durable rows are also what content protection encrypts.

A durable sessions row is also what session ownership writes its owner into. The column is added by a migration, is nullable, and stays NULL until you turn ownership on — so enabling persistence costs nothing here, and enabling ownership later is a configuration change rather than a data change. The listing filter is a WHERE clause on that column, applied before paging, which is precisely what an in-memory setup cannot offer.

  • Securing the endpoints — required reading before this leaves your machine.
  • Write your own store — implement IRunStore (or another store interface) against a persistence engine none of the three built-in providers cover.