@@ -220,6 +220,7 @@ export async function runReplayScriptFile(params: {
220220 }
221221
222222 const startedAt = Date . now ( ) ;
223+ const keepSession = req . flags ?. replayKeepSession === true ;
223224 let resolved = '' ;
224225 const artifactPaths = new Set < string > ( ) ;
225226 // #1478 P4b: the one locked coordinator this request reaches the repair
@@ -231,7 +232,7 @@ export async function runReplayScriptFile(params: {
231232 return errorResponse ( 'INVALID_ARGS' , maestroBackendRequiredMessage ( 'replay' , filePath ) ) ;
232233 }
233234 if ( resolveReplayFormat ( resolved , req . flags ?. replayBackend ) === 'maestro' ) {
234- if ( req . flags ?. replayKeepSession === true ) {
235+ if ( keepSession ) {
235236 return errorResponse (
236237 'INVALID_ARGS' ,
237238 '--keep-session is supported only for native .ad replay; Maestro YAML owns its lifecycle.' ,
@@ -252,6 +253,7 @@ export async function runReplayScriptFile(params: {
252253 tracePath,
253254 resolved,
254255 coordinator,
256+ keepSession,
255257 } ) ;
256258 if ( ! planPreparation . ok ) return planPreparation . response ;
257259 const {
@@ -264,6 +266,7 @@ export async function runReplayScriptFile(params: {
264266 scope,
265267 actionTracePath,
266268 snapshotDiagnosticSamples,
269+ suppressedTerminalCloseIndex,
267270 } = planPreparation . value ;
268271 const sessionPreparation = prepareReplaySession ( {
269272 req,
@@ -308,6 +311,7 @@ export async function runReplayScriptFile(params: {
308311 snapshotDiagnosticSamples,
309312 onStep,
310313 armSaveScript : sessionPreparation . armSaveScript ,
314+ suppressedTerminalCloseIndex,
311315 } ) ;
312316 if ( failure ) return failure ;
313317 return completeReplayRun ( {
@@ -320,6 +324,8 @@ export async function runReplayScriptFile(params: {
320324 snapshotDiagnosticSamples,
321325 armSaveScript : sessionPreparation . armSaveScript ,
322326 coordinator,
327+ keepSession,
328+ suppressedTerminalCloseIndex,
323329 } ) ;
324330 } catch ( err ) {
325331 const appErr = asAppError ( err ) ;
@@ -348,6 +354,7 @@ type ReplayActionExecution = {
348354 snapshotDiagnosticSamples : SnapshotTimingSample [ ] ;
349355 onStep : ReplayTestAttemptStepSink | undefined ;
350356 armSaveScript : ( ) => void ;
357+ suppressedTerminalCloseIndex : number | undefined ;
351358} ;
352359
353360async function executeReplayActions (
@@ -363,24 +370,15 @@ async function executeReplayActions(
363370 snapshotDiagnosticSamples,
364371 onStep,
365372 armSaveScript,
373+ suppressedTerminalCloseIndex,
366374 } = params ;
367375 for ( let index = entryIndex ; index < actions . length ; index += 1 ) {
368376 const action = actions [ index ] ;
369377 if ( ! isExecutableReplayAction ( action ) ) continue ;
370378 // Arm before checking terminal close so `[open, close]` records the
371379 // session created by `open` before treating `close` as lifecycle.
372380 armSaveScript ( ) ;
373- if (
374- shouldSkipTerminalClose ( {
375- action,
376- index,
377- totalActions : actions . length ,
378- keepSession : params . req . flags ?. replayKeepSession === true ,
379- coordinator : stepContext . coordinator ,
380- } )
381- ) {
382- continue ;
383- }
381+ if ( index === suppressedTerminalCloseIndex ) continue ;
384382 onStep ?.( replayActionStep ( index , actions . length , action ) ) ;
385383 const sampleStart = readSessionSnapshotSampleCount ( sessionStore , sessionName ) ;
386384 const response = await resolveReplayStepResponse ( stepContext , action , index , [
@@ -441,6 +439,8 @@ function completeReplayRun(params: {
441439 snapshotDiagnosticSamples : SnapshotTimingSample [ ] ;
442440 armSaveScript : ( ) => void ;
443441 coordinator : ReplayCoordinator ;
442+ keepSession : boolean ;
443+ suppressedTerminalCloseIndex : number | undefined ;
444444} ) : DaemonResponse {
445445 const {
446446 startedAt,
@@ -452,11 +452,24 @@ function completeReplayRun(params: {
452452 snapshotDiagnosticSamples,
453453 armSaveScript,
454454 coordinator,
455+ keepSession,
456+ suppressedTerminalCloseIndex,
455457 } = params ;
456458 armSaveScript ( ) ;
457459 coordinator . markCompleteIfArmed ( ) ;
458460 const completedSession = sessionStore . get ( sessionName ) ;
459- const replayedCount = actions . length - entryIndex ;
461+ if ( keepSession && ! completedSession ) {
462+ return errorResponse (
463+ 'COMMAND_FAILED' ,
464+ `Replay completed but --keep-session could not preserve session "${ sessionName } ". Run the script again after checking which action closed the session.` ,
465+ artifactPaths . size > 0 ? { artifactPaths : [ ...artifactPaths ] } : undefined ,
466+ ) ;
467+ }
468+ const replayedCount = countExecutedReplayActions ( {
469+ actions,
470+ entryIndex,
471+ suppressedTerminalCloseIndex,
472+ } ) ;
460473 const snapshotDiagnosticsSummary = summarizeSnapshotTimingSamples ( snapshotDiagnosticSamples ) ;
461474 return {
462475 ok : true ,
@@ -521,6 +534,7 @@ type PreparedReplayPlan = {
521534 scope : ReplayVarScope ;
522535 actionTracePath : string | undefined ;
523536 snapshotDiagnosticSamples : SnapshotTimingSample [ ] ;
537+ suppressedTerminalCloseIndex : number | undefined ;
524538} ;
525539
526540type ParsedReplayInput = ReturnType < typeof parseReplayInput > ;
@@ -532,8 +546,9 @@ function prepareReplayPlan(params: {
532546 tracePath : string | undefined ;
533547 resolved : string ;
534548 coordinator : ReplayCoordinator ;
549+ keepSession : boolean ;
535550} ) : { ok : true ; value : PreparedReplayPlan } | { ok : false ; response : DaemonResponse } {
536- const { req, sessionName, sessionStore, tracePath, resolved, coordinator } = params ;
551+ const { req, sessionName, sessionStore, tracePath, resolved, coordinator, keepSession } = params ;
537552 const parsedResult = parseReplayScript ( resolved , req ) ;
538553 if ( ! parsedResult . ok ) return parsedResult ;
539554 const parsed = parsedResult . value ;
@@ -571,6 +586,12 @@ function prepareReplayPlan(params: {
571586 scope : buildPreparedReplayScope ( { req, replayReq, sessionName, resolved, metadata } ) ,
572587 actionTracePath : tracePath ?? preEntrySession ?. trace ?. outPath ,
573588 snapshotDiagnosticSamples : [ ] ,
589+ suppressedTerminalCloseIndex : resolveSuppressedTerminalCloseIndex ( {
590+ actions,
591+ keepSession,
592+ saveScript : req . flags ?. saveScript ,
593+ repairActive : coordinator . view ( ) ?. repairBoundary !== undefined ,
594+ } ) ,
574595 } ,
575596 } ;
576597}
@@ -794,24 +815,37 @@ function preflightSaveScriptTarget(params: {
794815}
795816
796817/**
797- * The one native replay lifecycle seam for an authored terminal `close`.
798- * `--keep-session` suppresses it so callers can take over the live session;
799- * ADR 0012 repair suppresses it so the agent can finalize through
800- * `close --save-script`. Interior closes retain authored semantics. Repair is
801- * checked against session state (not only this leg's flags), preserving R2
802- * across separate `--from` continuations.
818+ * Resolves the one native replay lifecycle seam once per plan. Terminal means
819+ * the last executable action, because nested `replay` markers are plan
820+ * metadata and never dispatch. The suppressed close is therefore neither
821+ * divergence-checked nor included in the successful `replayed` count.
803822 */
804- function shouldSkipTerminalClose ( params : {
805- action : SessionAction ;
806- index : number ;
807- totalActions : number ;
823+ function resolveSuppressedTerminalCloseIndex ( params : {
824+ actions : SessionAction [ ] ;
808825 keepSession : boolean ;
809- coordinator : ReplayCoordinator ;
810- } ) : boolean {
811- const { action, index, totalActions, keepSession, coordinator } = params ;
812- if ( action . command !== 'close' ) return false ;
813- if ( index !== totalActions - 1 ) return false ;
814- return keepSession || coordinator . view ( ) ?. repairBoundary !== undefined ;
826+ saveScript : boolean | string | undefined ;
827+ repairActive : boolean ;
828+ } ) : number | undefined {
829+ if ( ! params . keepSession && ! params . saveScript && ! params . repairActive ) return undefined ;
830+ for ( let index = params . actions . length - 1 ; index >= 0 ; index -= 1 ) {
831+ const action = params . actions [ index ] ;
832+ if ( ! isExecutableReplayAction ( action ) ) continue ;
833+ return action . command === 'close' ? index : undefined ;
834+ }
835+ return undefined ;
836+ }
837+
838+ function countExecutedReplayActions ( params : {
839+ actions : SessionAction [ ] ;
840+ entryIndex : number ;
841+ suppressedTerminalCloseIndex : number | undefined ;
842+ } ) : number {
843+ let count = 0 ;
844+ for ( let index = params . entryIndex ; index < params . actions . length ; index += 1 ) {
845+ if ( index === params . suppressedTerminalCloseIndex ) continue ;
846+ if ( isExecutableReplayAction ( params . actions [ index ] ) ) count += 1 ;
847+ }
848+ return count ;
815849}
816850
817851/**
0 commit comments