@@ -12,6 +12,7 @@ import {
1212 ProjectId ,
1313 ProviderInstanceId ,
1414 ProviderThreadId ,
15+ RunAttemptId ,
1516 RunId ,
1617 ThreadId ,
1718} from "@t3tools/contracts" ;
@@ -52,6 +53,7 @@ const rootNodeId = NodeId.make("node:run-finalized-root");
5253const providerThreadId = ProviderThreadId . make ( "provider-thread:run-finalized" ) ;
5354const providerInstanceId = ProviderInstanceId . make ( "codex" ) ;
5455const checkpointId = CheckpointId . make ( "checkpoint:run-finalized" ) ;
56+ const attemptId = RunAttemptId . make ( "attempt:run-finalized" ) ;
5557
5658const maxAttempts = 2 ;
5759/** The worker's backoff after a capture's first failed attempt. */
@@ -445,6 +447,67 @@ it.effect.each(["interrupted", "cancelled"] as const)(
445447 } ) . pipe ( Effect . provide ( makeLayer ( commitCapture ( status ) ) ) ) ,
446448) ;
447449
450+ /** Ends a running attempt through the ownership guard, as RunExecutionService does. */
451+ const finishGuardedTurn = (
452+ status : "interrupted" | "cancelled" ,
453+ guard : { readonly activeAttemptId : RunAttemptId ; readonly expectedStatus : "running" | "waiting" } ,
454+ ) =>
455+ Effect . gen ( function * ( ) {
456+ const eventSink = yield * EventSink . EventSinkV2 ;
457+ const now = yield * DateTime . now ;
458+ yield * eventSink . write ( {
459+ events : [
460+ runEvent ( "run.updated" , makeRun ( now , "running" , { activeAttemptId : attemptId } ) , now ) ,
461+ ] ,
462+ } ) ;
463+ return yield * eventSink . writeIfRunCurrent ( {
464+ threadId,
465+ runId,
466+ ...guard ,
467+ events : [
468+ runEvent (
469+ "run.updated" ,
470+ makeRun ( now , status , { activeAttemptId : attemptId , completedAt : now } ) ,
471+ now ,
472+ ) ,
473+ ] ,
474+ effects : [ captureEffect ] ,
475+ } ) ;
476+ } ) ;
477+
478+ it . effect . each ( [ "interrupted" , "cancelled" ] as const ) (
479+ "a guarded stop that queues a capture finalizes as %s after the capture" ,
480+ ( status ) =>
481+ Effect . gen ( function * ( ) {
482+ yield * seedThread ;
483+ const result = yield * finishGuardedTurn ( status , {
484+ activeAttemptId : attemptId ,
485+ expectedStatus : "running" ,
486+ } ) ;
487+ assert . isTrue ( result . committed ) ;
488+ assert . lengthOf ( yield * finalizationRecords , 0 ) ;
489+ yield * drainWorker ;
490+ assert . deepEqual ( yield * finalizationRecords , [
491+ { type : "run.finalized" , payload : { runId, outcome : status , checkpointId } } ,
492+ ] ) ;
493+ } ) . pipe ( Effect . provide ( makeLayer ( commitCapture ( status ) ) ) ) ,
494+ ) ;
495+
496+ it . effect ( "a guarded stop that no longer owns the run commits and queues nothing" , ( ) =>
497+ Effect . gen ( function * ( ) {
498+ yield * seedThread ;
499+ const result = yield * finishGuardedTurn ( "interrupted" , {
500+ activeAttemptId : RunAttemptId . make ( "attempt:run-finalized-stale" ) ,
501+ expectedStatus : "running" ,
502+ } ) ;
503+ assert . isFalse ( result . committed ) ;
504+ assert . deepEqual ( yield * captureStatus , Option . none ( ) ) ;
505+ assert . equal ( yield * drainWorker , 0 ) ;
506+ assert . equal ( yield * runStatus , "running" ) ;
507+ assert . lengthOf ( yield * finalizationRecords , 0 ) ;
508+ } ) . pipe ( Effect . provide ( makeLayer ( commitCapture ( "interrupted" ) ) ) ) ,
509+ ) ;
510+
448511it . effect ( "a run that ends without a capture finalizes in its terminal commit" , ( ) =>
449512 Effect . gen ( function * ( ) {
450513 const eventSink = yield * EventSink . EventSinkV2 ;
0 commit comments