Class BowireRecordingSession

Namespace
Kuestenlogik.Bowire.Recording
Assembly
Kuestenlogik.Bowire.dll

Server-side active-recording lifecycle (#285). Lives in core as a singleton so multiple consumers — the workbench HTTP surface, the MCP tool surface (bowire.record.start / stop / replay), and the in-process invoke / channel pipelines that need to know "is there a recording open right now?" — share one source of truth instead of the pre-#285 browser-localStorage-as-truth pattern.

public sealed class BowireRecordingSession
Inheritance
BowireRecordingSession
Inherited Members

Remarks

Pre-#285 the active-recording state lived in the browser (bowire_recording_active, bowire_recording_buffer); the MCP server had no way to reach it, so the bowire.record.start / stop / replay tools shipped as deferred no-ops. Lifting the state into this singleton unblocks the tools without forcing the workbench to give up its localStorage cache for completed recordings (the bowire_recordings bucket stays — it's a flush sink, not the source of truth).

State machine: Idle → Active (Capture | Proxy | Replay) → Idle. Start(string, BowireRecordingMode, string?, string?) transitions Idle → Active and rejects if a session is already open (callers must Stop(Func<BowireRecording, BowireRecording>?) first). Stop(Func<BowireRecording, BowireRecording>?) flushes the buffer to the supplied sink and returns the persisted recording. SwitchToReplay() flips the mode in-place without dropping the buffer.

Subscribers (Subscribe()) receive a ChannelReader<T> of BowireRecordingSessionEvent; the SSE endpoint translates each event to a text/event-stream frame for the workbench's recorder UI. The channel is bounded with drop-oldest semantics — a wedged subscriber can't grow memory unbounded, and the workbench is interested in "latest state" not "every transition since it last polled".

Constructors

BowireRecordingSession()

public BowireRecordingSession()

Fields

SubscriberChannelCapacity

Bounded subscriber channel cap. 64 transitions is enough headroom for the slowest browser tab to drain while a CLI agent rapidly starts / stops; DropOldest matches "the UI cares about the latest session state", not "the UI needs every historical transition".

public const int SubscriberChannelCapacity = 64

Field Value

int

Properties

Active

Currently-active session, or null when the recorder is idle. Returns an immutable snapshot — callers can inspect without holding the gate. Mutating tools (AppendStep(BowireRecordingStep), Stop(Func<BowireRecording, BowireRecording>?)) go through the dedicated methods so the gate is held for the transition.

public BowireRecordingSessionState? Active { get; }

Property Value

BowireRecordingSessionState

Methods

AppendStep(BowireRecordingStep)

Append a captured step to the active session's buffer. Returns the step's 0-based index. Throws when no session is active.

public int AppendStep(BowireRecordingStep step)

Parameters

step BowireRecordingStep

Returns

int

Start(string, BowireRecordingMode, string?, string?)

Start a new recording session. Returns the freshly-created snapshot. Throws when a session is already open — callers must Stop(Func<BowireRecording, BowireRecording>?) or SwitchToReplay() first.

public BowireRecordingSessionState Start(string workspaceId, BowireRecordingMode mode, string? name = null, string? recordingId = null)

Parameters

workspaceId string

Workspace the recording lives under. Empty string is accepted (legacy unscoped workspaces), but the caller should prefer the active workspace id so multi-workspace operators don't see cross-leakage.

mode BowireRecordingMode

One of Capture (record live invocations), Proxy (record proxied flows), Replay (drive replay from a pre-existing recording).

name string

Optional human-readable name. Defaults to "Untitled recording" so the UI always has something to show.

recordingId string

Optional caller-supplied id (used by replay to bind to an existing recording). When null, a fresh rec_* id is generated.

Returns

BowireRecordingSessionState

Stop(Func<BowireRecording, BowireRecording>?)

Stop the active session and hand the assembled recording to the supplied flush sink. The sink's return value is propagated back so the caller (REST endpoint, MCP tool) can surface the persisted recording id. When flush is null the recording is returned without being persisted — useful for replay sessions that don't want a new entry in the recording store.

public BowireRecording? Stop(Func<BowireRecording, BowireRecording>? flush = null)

Parameters

flush Func<BowireRecording, BowireRecording>

Returns

BowireRecording

The final BowireRecording the session produced. null only if no session was active in the first place (idempotent stop).

Subscribe()

Subscribe to session-state-change events. Returns the channel reader the SSE producer streams from and an IDisposable that, when disposed, unsubscribes. Each subscriber gets a private bounded channel so a slow browser tab can't back-pressure other subscribers or the producer.

public (ChannelReader<BowireRecordingSessionEvent> Reader, IDisposable Subscription) Subscribe()

Returns

(ChannelReader<BowireRecordingSessionEvent> Reader, IDisposable Subscription)

SwitchToReplay()

Flip the active session into Replay. Buffer is preserved (replay reads from it). Throws when no session is active.

public BowireRecordingSessionState SwitchToReplay()

Returns

BowireRecordingSessionState