Skip to content

Inspecting and repairing

When an append is rejected with StreamClosed, you need to know what closed the scope, when, and whether that was intended. The client, the kernel contract and the Workbench all answer that.

IsStreamCompleted returns true when any closure covers the scope:

using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Events;
public class InspectingIsCompleted(IEventLog eventLog)
{
public async Task<bool> IsApril(EventSourceId reportId) =>
await eventLog.IsStreamCompleted(new ClosedStreamScope(
EventSourceId: reportId,
EventStreamType: new EventStreamType("monthly-report"),
EventStreamId: new EventStreamId("2026-04")));
}

View C# snippet source on GitHub

It uses coverage, the same rule as an append. A scope is closed when a closure with equal or fewer dimensions matches its values, so asking about a stream of a closed event source returns true. The two-argument overload IsStreamCompleted(streamType, streamId) asks the same question for a stream type and id pair, with empty values mapped to All and Default.

GetClosedStreams returns every closure of the event sequence, or those inside the scope you pass:

using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Events;
public class InspectingClosedStreams(IEventLog eventLog)
{
public async Task Print(EventSourceId reportId)
{
// Everything closed for one event source. Omit the argument to list every closure.
var closedStreams = await eventLog.GetClosedStreams(ClosedStreamScope.ForEventSource(reportId));
foreach (var closed in closedStreams)
{
var owner = closed.Origin == ClosedStreamOrigin.ClosingEvent
? $"closing event constraint '{closed.ClosedBy}'"
: "manual CompleteStream";
Console.WriteLine($"{closed.Scope} closed at {closed.SequenceNumber} ({closed.ClosedAt}) by {owner}");
}
}
}

View C# snippet source on GitHub

Each ClosedStream has:

PropertyMeaning
ScopeThe closed dimensions
OriginCompleteStream for a manual closure, ClosingEvent for an event-driven one
ClosedByThe closing constraint’s name. Null for a manual closure
SequenceNumberThe sequence number at closure. EventSequenceNumber.Unavailable for a closure written by an earlier version
ClosedAtThe closing time. Null for a closure written by an earlier version

The client call is not paged. It returns every match, so pass a scope to narrow it on a large event sequence.

The Workbench has a read-only list of closed streams for each event sequence. It shows the same fields as GetClosedStreams and is the quickest way to find out why an append was refused. See the Workbench.

Closing is meant to be final, so the client interface has no reopen call. An operator can repair a mistaken manual close through the kernel’s ReopenStreamScope command on the event sequences gRPC service. Call it from a tool that talks to the kernel contracts; it is not part of the .NET client.

The command:

  • removes only an exact manual closure for the scope you give. It does not narrow, widen or split a closure;
  • needs a reason, and an authenticated principal that has a subject. The kernel has no role model, so who may call it is decided by your authentication and network setup;
  • appends a StreamScopeReopened event to the system event sequence before it removes the closure. The event holds the event sequence, the scope, the reason and the actor, so the repair stays visible;
  • returns NotCompleted when no manual closure matches, and ClosedByEvent when the closure belongs to a closing constraint.

An event-driven closure is not repaired this way. Append one of its ReopenedBy events instead, see Closing events.

  • Closing streams for the policy: manual closures are final, event-driven closures reopen with events.
  • Scoped closing to close with an expected tail so you do not need to repair.