@@ -30,8 +30,10 @@ export class ExecutionSession {
3030 readonly graph : HydratedGraphData ;
3131
3232 private readonly runner : WorkflowRunner ;
33- private readonly stream : MessageStream ;
33+ /** Null unless the caller opted into `captureMessages` (see options). */
34+ private readonly stream : MessageStream | null ;
3435 private readonly persistence : ExecutionSessionOptions [ "persistence" ] ;
36+ private readonly bridge : { pendingRequestCount ?: number } | null ;
3537 private readonly resultPromise : Promise < RunResult > ;
3638 private runTimeoutHandle : ReturnType < typeof setTimeout > | null = null ;
3739 private _cancelReason : string | null = null ;
@@ -45,15 +47,21 @@ export class ExecutionSession {
4547 persistence : ExecutionSessionOptions [ "persistence" ] ;
4648 params : Record < string , unknown > ;
4749 triggerEvent : ExecutionSessionOptions [ "triggerEvent" ] ;
50+ bridge : { pendingRequestCount ?: number } | null ;
4851 closeBridge : ( ) => void ;
4952 runTimeoutMs : number | undefined ;
53+ captureMessages : boolean ;
54+ messageBufferLimit : number | undefined ;
5055 } ) {
5156 this . jobId = init . jobId ;
5257 this . workflowId = init . workflowId ;
5358 this . graph = init . graph ;
5459 this . runner = init . runner ;
5560 this . persistence = init . persistence ;
56- this . stream = new MessageStream ( init . context ) ;
61+ this . bridge = init . bridge ;
62+ this . stream = init . captureMessages
63+ ? new MessageStream ( init . context , init . messageBufferLimit )
64+ : null ;
5765
5866 if ( init . runTimeoutMs && init . runTimeoutMs > 0 ) {
5967 this . runTimeoutHandle = setTimeout ( ( ) => {
@@ -80,7 +88,7 @@ export class ExecutionSession {
8088 // The terminal message was emitted synchronously before run()
8189 // resolved (ProcessingContext.emit() calls listeners inline), so the
8290 // stream can close now without dropping it.
83- this . stream . close ( ) ;
91+ this . stream ? .close ( ) ;
8492 } ) ;
8593
8694 this . resultPromise
@@ -206,13 +214,28 @@ export class ExecutionSession {
206214 persistence : options . persistence ?? null ,
207215 params : options . params ?? { } ,
208216 triggerEvent : options . triggerEvent ?? null ,
217+ bridge,
209218 closeBridge,
210- runTimeoutMs : options . limits ?. runTimeoutMs
219+ runTimeoutMs : options . limits ?. runTimeoutMs ,
220+ captureMessages : options . captureMessages === true ,
221+ messageBufferLimit : options . limits ?. messageBufferLimit
211222 } ) ;
212223 }
213224
214- /** Validated, live message stream — closes once the run reaches a terminal state. */
225+ /**
226+ * Live message stream — closes once the run reaches a terminal state.
227+ * Requires `captureMessages: true` at `create()`; without it nothing is
228+ * queued (see that option) and reading this throws rather than handing back
229+ * a stream that would silently yield nothing.
230+ */
215231 get messages ( ) : AsyncIterable < ProcessingMessage > {
232+ if ( ! this . stream ) {
233+ throw new Error (
234+ "ExecutionSession: `messages` requires `captureMessages: true` at " +
235+ "create() — message capture is opt-in so a host that only awaits " +
236+ "`result` never queues a run's messages unread."
237+ ) ;
238+ }
216239 return this . stream ;
217240 }
218241
@@ -221,6 +244,31 @@ export class ExecutionSession {
221244 return this . resultPromise ;
222245 }
223246
247+ /**
248+ * Live resource counts, for leak accounting: after a terminal result every
249+ * one of these must be back to zero. Measured, not inferred — the
250+ * reliability harness's `cleanup-leaks` invariant asserts against these
251+ * numbers and reports a violation when a driver can't produce them.
252+ */
253+ resourceCounters ( ) : {
254+ liveActors : number ;
255+ pendingControlResponses : number ;
256+ pendingTimers : number ;
257+ pythonBridgePendingRequests : number ;
258+ } {
259+ return {
260+ liveActors : this . runner . liveActorCount ,
261+ pendingControlResponses : this . runner . pendingControlResponseCount ,
262+ // The session's own run-timeout timer is the only timer it owns; it is
263+ // cleared when the run settles.
264+ pendingTimers : this . runTimeoutHandle === null ? 0 : 1 ,
265+ pythonBridgePendingRequests :
266+ typeof this . bridge ?. pendingRequestCount === "number"
267+ ? this . bridge . pendingRequestCount
268+ : 0
269+ } ;
270+ }
271+
224272 /** The reason passed to the most recent `cancel()` call, if any. */
225273 get cancelReason ( ) : string | null {
226274 return this . _cancelReason ;
0 commit comments