Skip to content
Tracon

IRunStore

Namespace Tracon · Assembly Tracon.Abstractions.dll

Store for run records and the event stream.

public interface IRunStore

Events are append-only. An implementation must never update or delete an event; it only adds them through IRunStore.AppendEventAsync.

Important: a failure in this store never interrupts the run. Observability must not break functionality. The caller (RunEventWriter) catches and logs the failure.

Tenant behavior is not uniform across this interface’s methods; each falls into one of three modes:

ModeMethods and rule
Expected tenant [IRunStore.AppendEventAsync](/api/tracon.irunstore/#Tracon_IRunStore_AppendEventAsync_Tracon_RunEvent_System_Threading_CancellationToken_), [IRunStore.CompleteRunAsync](/api/tracon.irunstore/#Tracon_IRunStore_CompleteRunAsync_Tracon_RunCompletion_System_Threading_CancellationToken_), [IRunStore.UpdateRunCostAsync](/api/tracon.irunstore/#Tracon_IRunStore_UpdateRunCostAsync_System_Guid_Tracon_RunCost_System_String_System_Threading_CancellationToken_). The tenant carried by the call is compared against the record's own tenant; a mismatch fails the write (or, for [IRunStore.UpdateRunCostAsync](/api/tracon.irunstore/#Tracon_IRunStore_UpdateRunCostAsync_System_Guid_Tracon_RunCost_System_String_System_Threading_CancellationToken_), silently affects no row). The ambient tenant is **not** consulted — a workflow or job queue writes on behalf of a run whose tenant may differ from whatever is ambient on the calling thread. A [null](https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/null) tenant on the call performs no check.
Ambient tenant [IRunStore.GetRunAsync](/api/tracon.irunstore/#Tracon_IRunStore_GetRunAsync_System_Guid_System_Threading_CancellationToken_), [IRunStore.QueryRunsAsync](/api/tracon.irunstore/#Tracon_IRunStore_QueryRunsAsync_Tracon_RunQuery_System_Threading_CancellationToken_), [IRunStore.ReadEventsAsync](/api/tracon.irunstore/#Tracon_IRunStore_ReadEventsAsync_System_Guid_System_Int64_System_Threading_CancellationToken_), [IRunStore.GetStatisticsAsync](/api/tracon.irunstore/#Tracon_IRunStore_GetStatisticsAsync_Tracon_RunStatisticsQuery_System_Threading_CancellationToken_), [IRunStore.GetToolUsageAsync](/api/tracon.irunstore/#Tracon_IRunStore_GetToolUsageAsync_Tracon_ToolUsageQuery_System_Threading_CancellationToken_), [IRunStore.GetExperimentResultsAsync](/api/tracon.irunstore/#Tracon_IRunStore_GetExperimentResultsAsync_Tracon_ExperimentResultsQuery_System_Threading_CancellationToken_), [IRunStore.GetTimeSeriesAsync](/api/tracon.irunstore/#Tracon_IRunStore_GetTimeSeriesAsync_Tracon_RunTimeSeriesQuery_System_Threading_CancellationToken_), [IRunStore.ListToolInvocationsAsync](/api/tracon.irunstore/#Tracon_IRunStore_ListToolInvocationsAsync_System_Guid_System_Threading_CancellationToken_), [IRunStore.RecordToolInvocationAsync](/api/tracon.irunstore/#Tracon_IRunStore_RecordToolInvocationAsync_Tracon_ToolInvocationRecord_System_Threading_CancellationToken_). Filtered by the current `ITenantContext` unless the query object itself carries an explicit tenant override.
Tenant-independent [IRunStore.TouchHeartbeatAsync](/api/tracon.irunstore/#Tracon_IRunStore_TouchHeartbeatAsync_System_Collections_Generic_IReadOnlyCollection_System_Guid__System_DateTimeOffset_System_Threading_CancellationToken_), [IRunStore.ClaimOrphanedRunsAsync](/api/tracon.irunstore/#Tracon_IRunStore_ClaimOrphanedRunsAsync_System_DateTimeOffset_System_Int32_System_Threading_CancellationToken_). Maintenance work that scans every tenant's rows; see each method's own remarks.

Thread safety. An implementation is registered as a singleton and must be safe under concurrent calls from unrelated runs. It must not capture or depend on a scoped service — the same instance answers every run’s calls for the lifetime of the process.

Null / not-found semantics differ by method and are documented individually; as a summary:

MethodAn id that does not exist
[IRunStore.GetRunAsync](/api/tracon.irunstore/#Tracon_IRunStore_GetRunAsync_System_Guid_System_Threading_CancellationToken_)[null](https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/null)
[IRunStore.ReadEventsAsync](/api/tracon.irunstore/#Tracon_IRunStore_ReadEventsAsync_System_Guid_System_Int64_System_Threading_CancellationToken_)An empty sequence — never throws
[IRunStore.AppendEventAsync](/api/tracon.irunstore/#Tracon_IRunStore_AppendEventAsync_Tracon_RunEvent_System_Threading_CancellationToken_)Throws [TraconException](/api/tracon.traconexception/)
[IRunStore.CompleteRunAsync](/api/tracon.irunstore/#Tracon_IRunStore_CompleteRunAsync_Tracon_RunCompletion_System_Threading_CancellationToken_)Throws [TraconException](/api/tracon.traconexception/)

AppendEventAsync(RunEvent, CancellationToken)

Section titled “ AppendEventAsync(RunEvent, CancellationToken)”

Appends an event to the run stream.

ValueTask AppendEventAsync(RunEvent runEvent, CancellationToken cancellationToken = default)

runEvent RunEvent

The event to append. The caller assigns its sequence number.

cancellationToken CancellationToken

The cancellation token.

ValueTask

A task that completes when the event is written.

Tenant behavior — EXPECTED tenant. When RunEvent.TenantId is set, the write is filtered by it: the event is written only if the run’s own tenant matches. The ambient tenant (ITenantContext) is deliberately not consulted for this method — a workflow or a background job queue legitimately writes on behalf of a run whose tenant differs from whichever tenant happens to be ambient on the calling thread. A null RunEvent.TenantId performs no tenant check at all.

Null / not-found semantics. Unlike IRunStore.ReadEventsAsync (which returns an empty sequence for an unknown run), this method throws TraconException when RunEvent.RunId does not identify an existing run, or when it exists but does not belong to the expected tenant.

Event order. RunEvent.Sequence is assigned by the caller, not by this method — the store only writes it. The event stream is append-only: an implementation must never update or delete a written event. A second call carrying a RunEvent.Sequence value already used by this run is a caller error, not a legitimate retry, and must be rejected with TraconException rather than silently accepted or silently ignored — an implementation must not let a duplicate sequence number pass through as a raw driver exception either.

ClaimOrphanedRunsAsync(DateTimeOffset, int, CancellationToken)

Section titled “ ClaimOrphanedRunsAsync(DateTimeOffset, int, CancellationToken)”

Closes Running rows that have not sent a heartbeat for a long time as Failed and records the reason.

ValueTask<IReadOnlyList<RunRecord>> ClaimOrphanedRunsAsync(DateTimeOffset staleBefore, int max, CancellationToken cancellationToken = default)

staleBefore DateTimeOffset

Running rows whose last heartbeat is older than this — or that never sent one — count as orphaned (UTC).

max int

The maximum number of rows to close in this round.

cancellationToken CancellationToken

The cancellation token.

ValueTask<IReadOnlyList<RunRecord>>

The records of the closed runs.

Closing also writes a RunEventType.RunFailed event to the stream — the process that was writing the run is gone, so this method emits the event itself. The sequence number is one past the current maximum.

Only Running rows are affected; Queued rows are owned by the job queue and this method DOES not TOUCH them.

[TenantAgnostic]: this is maintenance work and scans the orphaned rows of every tenant. Filtering by the ambient tenant would leave the rows of other tenants Running forever.

CompleteRunAsync(RunCompletion, CancellationToken)

Section titled “ CompleteRunAsync(RunCompletion, CancellationToken)”

Closes the run and updates its summary.

ValueTask CompleteRunAsync(RunCompletion completion, CancellationToken cancellationToken = default)

completion RunCompletion

The completion information.

cancellationToken CancellationToken

The cancellation token.

ValueTask

A task that completes when the record is updated.

GetExperimentResultsAsync(ExperimentResultsQuery, CancellationToken)

Section titled “ GetExperimentResultsAsync(ExperimentResultsQuery, CancellationToken)”

Summarizes an experiment per variant: count, failure rate, tokens, duration.

ValueTask<IReadOnlyList<ExperimentVariantResult>> GetExperimentResultsAsync(ExperimentResultsQuery query, CancellationToken cancellationToken = default)

query ExperimentResultsQuery

The filter.

cancellationToken CancellationToken

The cancellation token.

ValueTask<IReadOnlyList<ExperimentVariantResult>>

Per-variant results. A variant that took no traffic is absent.

The experiment breakdown is available only through this query; experiment_id is not emitted as a metric tag, for cardinality reasons.

Gets a run record.

ValueTask<RunRecord?> GetRunAsync(Guid runId, CancellationToken cancellationToken = default)

runId Guid

The run id.

cancellationToken CancellationToken

The cancellation token.

ValueTask<RunRecord?>

The record, or null when it does not exist.

GetStatisticsAsync(RunStatisticsQuery, CancellationToken)

Section titled “ GetStatisticsAsync(RunStatisticsQuery, CancellationToken)”

Summarizes runs.

ValueTask<RunStatistics> GetStatisticsAsync(RunStatisticsQuery query, CancellationToken cancellationToken = default)

query RunStatisticsQuery

The filter.

cancellationToken CancellationToken

The cancellation token.

ValueTask<RunStatistics>

Counts, token totals and the per-agent breakdown.

The summary is computed inside the store. Fetching records and aggregating them in memory would cover only a paged subset and give the wrong answer.

GetTimeSeriesAsync(RunTimeSeriesQuery, CancellationToken)

Section titled “ GetTimeSeriesAsync(RunTimeSeriesQuery, CancellationToken)”

Produces the per-bucket time series of runs, failures, tokens and cost.

ValueTask<IReadOnlyList<TimeSeriesPoint>> GetTimeSeriesAsync(RunTimeSeriesQuery query, CancellationToken cancellationToken = default)

query RunTimeSeriesQuery

The filter.

cancellationToken CancellationToken

The cancellation token.

ValueTask<IReadOnlyList<TimeSeriesPoint>>

The ordered bucket list. Empty buckets are returned too, with zero counts.

TraconException

The requested range exceeds RunTimeSeriesBucketing.MaxBuckets.

GetToolUsageAsync(ToolUsageQuery, CancellationToken)

Section titled “ GetToolUsageAsync(ToolUsageQuery, CancellationToken)”

Summarizes usage per tool.

ValueTask<IReadOnlyList<ToolUsage>> GetToolUsageAsync(ToolUsageQuery query, CancellationToken cancellationToken = default)

query ToolUsageQuery

The filter.

cancellationToken CancellationToken

The cancellation token.

ValueTask<IReadOnlyList<ToolUsage>>

Call counts, failure rates and average durations.

Same reason as IRunStore.GetStatisticsAsync: the summary is computed inside the store.

ListToolInvocationsAsync(Guid, CancellationToken)

Section titled “ ListToolInvocationsAsync(Guid, CancellationToken)”

Lists the tool calls of a run in chronological order.

ValueTask<IReadOnlyList<ToolInvocationRecord>> ListToolInvocationsAsync(Guid runId, CancellationToken cancellationToken = default)

runId Guid

The run id.

cancellationToken CancellationToken

The cancellation token.

ValueTask<IReadOnlyList<ToolInvocationRecord>>

The call records.

QueryRunsAsync(RunQuery, CancellationToken)

Section titled “ QueryRunsAsync(RunQuery, CancellationToken)”

Lists runs that match a filter, newest first.

ValueTask<IReadOnlyList<RunRecord>> QueryRunsAsync(RunQuery query, CancellationToken cancellationToken = default)

query RunQuery

The filter.

cancellationToken CancellationToken

The cancellation token.

ValueTask<IReadOnlyList<RunRecord>>

The matching records.

ReadEventsAsync(Guid, long, CancellationToken)

Section titled “ ReadEventsAsync(Guid, long, CancellationToken)”

Reads the events of a run in sequence order. Live streaming and historical replay take the same path.

IAsyncEnumerable<RunEvent> ReadEventsAsync(Guid runId, long fromSequence = 0, CancellationToken cancellationToken = default)

runId Guid

The run id.

fromSequence long

Reading starts at this sequence number, inclusive.

cancellationToken CancellationToken

The cancellation token.

IAsyncEnumerable<RunEvent>

The ordered event stream.

RecordToolInvocationAsync(ToolInvocationRecord, CancellationToken)

Section titled “ RecordToolInvocationAsync(ToolInvocationRecord, CancellationToken)”

Records a settled tool call.

ValueTask RecordToolInvocationAsync(ToolInvocationRecord invocation, CancellationToken cancellationToken = default)

invocation ToolInvocationRecord

The call summary.

cancellationToken CancellationToken

The cancellation token.

ValueTask

A task that completes when the record is written.

Kept apart from the event stream because durations and per-tool totals must be queryable without scanning the whole event stream.

StartRunAsync(RunStartInfo, CancellationToken)

Section titled “ StartRunAsync(RunStartInfo, CancellationToken)”

Opens a new run record, or updates it if one already exists.

ValueTask<RunRecord> StartRunAsync(RunStartInfo info, CancellationToken cancellationToken = default)

info RunStartInfo

The start information.

cancellationToken CancellationToken

The cancellation token.

ValueTask<RunRecord>

The effective record as it now stands, persisted. RunRecord.UserId and RunRecord.Labels reflect this call’s own COALESCE against the previous row (see Idempotency below), not necessarily the values this call was given.

Idempotency. This method is an UPSERT: calling it a second time with a RunStartInfo.RunId that already exists does not open a second row, it updates the existing one. This exists because a queued run is written twice — a placeholder row is written first (typically RunStatus.Queued), then rewritten once a worker actually starts it (typically RunStatus.Running).

Every field is overwritten by the second call except RunStartInfo.UserId and RunStartInfo.Labels, which are COALESCED: a null value on the second call leaves the previous value in place, a non-null value replaces it. The reason is attribution: the first write (typically inside the HTTP request, where the caller’s identity is known) may know the user; the second write (typically a background worker, which has no request to read identity from) usually does not, and must not erase what the first write recorded.

A conforming implementation must apply this same COALESCE rule to the value it returns — a caller that reads the return value instead of re-querying the record must see the same attribution a IRunStore.GetRunAsync call would.

TouchHeartbeatAsync(IReadOnlyCollection<Guid>, DateTimeOffset, CancellationToken)

Section titled “ TouchHeartbeatAsync(IReadOnlyCollection<Guid>, DateTimeOffset, CancellationToken)”

Updates the “still here” mark of in-flight runs in one batch.

ValueTask TouchHeartbeatAsync(IReadOnlyCollection<Guid> runIds, DateTimeOffset at, CancellationToken cancellationToken = default)

runIds IReadOnlyCollection<Guid>

The ids of the runs to mark.

at DateTimeOffset

The mark time (UTC).

cancellationToken CancellationToken

The cancellation token.

ValueTask

A task that completes when the marks are written.

Only Running rows are affected; an id that does not exist or is in another status is skipped silently — this is a maintenance signal and must not interrupt a run.

[TenantAgnostic]: the ids come from the calling process’s own IRunCancellationRegistry and are therefore already limited to the runs that process actually executes. A tenant filter would also need one query per tenant, which defeats the point of a heartbeat: a cheap signal that stays off the hot path.

UpdateRunCostAsync(Guid, RunCost?, string?, CancellationToken)

Section titled “ UpdateRunCostAsync(Guid, RunCost?, string?, CancellationToken)”

Updates the cost of a run. Used only by the maintenance endpoint (POST /api/stats/recalculate-costs); on the normal path the cost is written once by IRunStore.CompleteRunAsync.

ValueTask UpdateRunCostAsync(Guid runId, RunCost? cost, string? tenantId = null, CancellationToken cancellationToken = default)

runId Guid

The run id.

cost RunCost?

The new cost. May be null.

tenantId string?

The EXPECTED tenant of the run. Defence in depth; when null no tenant check is made. TenantId.

cancellationToken CancellationToken

The cancellation token.

ValueTask

A task that completes when the record is updated.