.NET API
IRunStore
Tracon.Abstractions.dllStore for run records and the event stream.
public interface IRunStoreRemarks
Section titled “Remarks”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:
| Mode | Methods 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:
| Method | An 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/) |
Methods
Section titled “Methods”AppendEventAsync(RunEvent, CancellationToken)
Section titled “ AppendEventAsync(RunEvent, CancellationToken)”Appends an event to the run stream.
ValueTask AppendEventAsync(RunEvent runEvent, CancellationToken cancellationToken = default)Parameters
Section titled “Parameters”runEvent RunEvent
The event to append. The caller assigns its sequence number.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”A task that completes when the event is written.
Remarks
Section titled “Remarks”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)Parameters
Section titled “Parameters”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.
Returns
Section titled “Returns”ValueTask<IReadOnlyList<RunRecord>>
The records of the closed runs.
Remarks
Section titled “Remarks”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)Parameters
Section titled “Parameters”completion RunCompletion
The completion information.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”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)Parameters
Section titled “Parameters”query ExperimentResultsQuery
The filter.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”ValueTask<IReadOnlyList<ExperimentVariantResult>>
Per-variant results. A variant that took no traffic is absent.
Remarks
Section titled “Remarks”The experiment breakdown is available only through this query;
experiment_id is not emitted as a metric tag, for cardinality reasons.
GetRunAsync(Guid, CancellationToken)
Section titled “ GetRunAsync(Guid, CancellationToken)”Gets a run record.
ValueTask<RunRecord?> GetRunAsync(Guid runId, CancellationToken cancellationToken = default)Parameters
Section titled “Parameters”runId Guid
The run id.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”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)Parameters
Section titled “Parameters”query RunStatisticsQuery
The filter.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”Counts, token totals and the per-agent breakdown.
Remarks
Section titled “Remarks”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)Parameters
Section titled “Parameters”query RunTimeSeriesQuery
The filter.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”ValueTask<IReadOnlyList<TimeSeriesPoint>>
The ordered bucket list. Empty buckets are returned too, with zero counts.
Exceptions
Section titled “Exceptions”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)Parameters
Section titled “Parameters”query ToolUsageQuery
The filter.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”ValueTask<IReadOnlyList<ToolUsage>>
Call counts, failure rates and average durations.
Remarks
Section titled “Remarks”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)Parameters
Section titled “Parameters”runId Guid
The run id.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”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)Parameters
Section titled “Parameters”query RunQuery
The filter.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”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)Parameters
Section titled “Parameters”runId Guid
The run id.
fromSequence long
Reading starts at this sequence number, inclusive.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”The ordered event stream.
RecordToolInvocationAsync(ToolInvocationRecord, CancellationToken)
Section titled “ RecordToolInvocationAsync(ToolInvocationRecord, CancellationToken)”Records a settled tool call.
ValueTask RecordToolInvocationAsync(ToolInvocationRecord invocation, CancellationToken cancellationToken = default)Parameters
Section titled “Parameters”invocation ToolInvocationRecord
The call summary.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”A task that completes when the record is written.
Remarks
Section titled “Remarks”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)Parameters
Section titled “Parameters”info RunStartInfo
The start information.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”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.
Remarks
Section titled “Remarks”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)Parameters
Section titled “Parameters”runIds IReadOnlyCollection<Guid>
The ids of the runs to mark.
The mark time (UTC).
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”A task that completes when the marks are written.
Remarks
Section titled “Remarks”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)Parameters
Section titled “Parameters”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.
Returns
Section titled “Returns”A task that completes when the record is updated.