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.
Check a scope
Section titled “Check a scope”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")));}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.
List closures
Section titled “List closures”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}"); } }}Each ClosedStream has:
| Property | Meaning |
|---|---|
Scope | The closed dimensions |
Origin | CompleteStream for a manual closure, ClosingEvent for an event-driven one |
ClosedBy | The closing constraint’s name. Null for a manual closure |
SequenceNumber | The sequence number at closure. EventSequenceNumber.Unavailable for a closure written by an earlier version |
ClosedAt | The 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.
See closures in the Workbench
Section titled “See closures in the Workbench”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.
Repair a mistaken close
Section titled “Repair a mistaken close”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
StreamScopeReopenedevent 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
NotCompletedwhen no manual closure matches, andClosedByEventwhen 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.
Related
Section titled “Related”- 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.