@@ -622,6 +622,189 @@ describe("SupervisorManager cycle triggers", () => {
622622 ) ;
623623 } ) ;
624624
625+ it ( "keeps a superseded target meta state when an old in-flight cycle later stops" , async ( ) => {
626+ const supervisor = await manager . create ( {
627+ sessionId : "sess-objective-stop-race" ,
628+ workspaceId : "ws-1" ,
629+ objective : "Initial objective" ,
630+ evaluatorProviderId : "codex" ,
631+ } ) ;
632+
633+ const targetMetaById = new Map < string , Record < string , unknown > > ( [
634+ [
635+ supervisor . targetId ,
636+ {
637+ targetId : supervisor . targetId ,
638+ sessionId : supervisor . sessionId ,
639+ workspaceId : supervisor . workspaceId ,
640+ objective : supervisor . objective ,
641+ status : "active" ,
642+ createdAt : 1 ,
643+ updatedAt : 1 ,
644+ supersededBy : null ,
645+ completedAt : null ,
646+ } ,
647+ ] ,
648+ ] ) ;
649+
650+ deps . targetStore . readTargetMeta . mockImplementation (
651+ async ( _workspacePath : string , targetId : string ) => {
652+ const meta = targetMetaById . get ( targetId ) ;
653+ if ( ! meta ) {
654+ throw new Error ( `Missing target meta for ${ targetId } ` ) ;
655+ }
656+ return { ...meta } ;
657+ }
658+ ) ;
659+ deps . targetStore . createTargetFiles . mockImplementation (
660+ async (
661+ _workspacePath : string ,
662+ input : {
663+ targetId : string ;
664+ sessionId : string ;
665+ workspaceId : string ;
666+ objective : string ;
667+ createdAt : number ;
668+ }
669+ ) => {
670+ targetMetaById . set ( input . targetId , {
671+ targetId : input . targetId ,
672+ sessionId : input . sessionId ,
673+ workspaceId : input . workspaceId ,
674+ objective : input . objective ,
675+ status : "active" ,
676+ createdAt : input . createdAt ,
677+ updatedAt : input . createdAt ,
678+ supersededBy : null ,
679+ completedAt : null ,
680+ } ) ;
681+ }
682+ ) ;
683+ deps . targetStore . markTargetSuperseded . mockImplementation (
684+ async ( _workspacePath : string , targetId : string , nextTargetId : string , updatedAt : number ) => {
685+ const current = targetMetaById . get ( targetId ) ;
686+ if ( ! current ) {
687+ throw new Error ( `Missing target meta for ${ targetId } ` ) ;
688+ }
689+ targetMetaById . set ( targetId , {
690+ ...current ,
691+ status : "superseded" ,
692+ supersededBy : nextTargetId ,
693+ updatedAt,
694+ } ) ;
695+ }
696+ ) ;
697+ deps . targetStore . saveTargetMeta . mockImplementation (
698+ async ( _workspacePath : string , targetId : string , meta : Record < string , unknown > ) => {
699+ targetMetaById . set ( targetId , { ...meta , targetId } ) ;
700+ }
701+ ) ;
702+
703+ let resolveEvaluation : ( ( result : SupervisorEvaluationResult ) => void ) | null = null ;
704+ vi . spyOn ( getManagerInternals ( ) . evaluator , "evaluate" ) . mockImplementationOnce (
705+ async ( ) =>
706+ await new Promise < SupervisorEvaluationResult > ( ( resolve ) => {
707+ resolveEvaluation = resolve ;
708+ } )
709+ ) ;
710+
711+ const cycle = await manager . triggerEvaluation ( supervisor . id ) ;
712+
713+ await waitFor ( ( ) => {
714+ expect ( resolveEvaluation ) . not . toBeNull ( ) ;
715+ expect ( manager . get ( supervisor . id ) ?. state ) . toBe ( "evaluating" ) ;
716+ } ) ;
717+
718+ const rotated = await manager . update ( supervisor . id , {
719+ objective : "New objective" ,
720+ } ) ;
721+
722+ resolveEvaluation ?.( {
723+ status : "stop" ,
724+ stopReason : "objective_complete" ,
725+ reason : "old objective is done" ,
726+ } ) ;
727+
728+ await waitFor ( ( ) => {
729+ const finished = manager . get ( supervisor . id ) ?. cycles . find ( ( entry ) => entry . id === cycle . id ) ;
730+ expect ( finished ?. status ) . toBe ( "completed" ) ;
731+ } ) ;
732+
733+ expect ( targetMetaById . get ( supervisor . targetId ) ) . toMatchObject ( {
734+ status : "superseded" ,
735+ supersededBy : rotated . targetId ,
736+ } ) ;
737+ expect ( targetMetaById . get ( rotated . targetId ) ) . toMatchObject ( {
738+ status : "active" ,
739+ } ) ;
740+ } ) ;
741+
742+ it ( "rolls back a supersede mark when creating the next target files fails" , async ( ) => {
743+ const supervisor = await manager . create ( {
744+ sessionId : "sess-objective-create-fails" ,
745+ workspaceId : "ws-1" ,
746+ objective : "Initial objective" ,
747+ evaluatorProviderId : "codex" ,
748+ } ) ;
749+
750+ let latestMeta : Record < string , unknown > = {
751+ targetId : supervisor . targetId ,
752+ sessionId : supervisor . sessionId ,
753+ workspaceId : supervisor . workspaceId ,
754+ objective : supervisor . objective ,
755+ status : "active" ,
756+ createdAt : 1 ,
757+ updatedAt : 1 ,
758+ supersededBy : null ,
759+ completedAt : null ,
760+ } ;
761+
762+ deps . targetStore . readTargetMeta . mockImplementation ( async ( ) => ( { ...latestMeta } ) ) ;
763+ deps . targetStore . markTargetSuperseded . mockImplementation (
764+ async ( _workspacePath : string , targetId : string , nextTargetId : string , updatedAt : number ) => {
765+ latestMeta = {
766+ ...latestMeta ,
767+ targetId,
768+ status : "superseded" ,
769+ supersededBy : nextTargetId ,
770+ updatedAt,
771+ } ;
772+ }
773+ ) ;
774+ deps . targetStore . saveTargetMeta . mockImplementation (
775+ async ( _workspacePath : string , targetId : string , meta : Record < string , unknown > ) => {
776+ latestMeta = { ...meta , targetId } ;
777+ }
778+ ) ;
779+
780+ const createTargetFilesError = new Error ( "disk full" ) ;
781+ deps . targetStore . createTargetFiles . mockImplementation ( async ( ) => {
782+ throw createTargetFilesError ;
783+ } ) ;
784+
785+ await expect (
786+ manager . update ( supervisor . id , {
787+ objective : "New objective" ,
788+ } )
789+ ) . rejects . toThrow ( "disk full" ) ;
790+
791+ expect ( manager . get ( supervisor . id ) ?. targetId ) . toBe ( supervisor . targetId ) ;
792+ expect ( latestMeta ) . toMatchObject ( {
793+ targetId : supervisor . targetId ,
794+ status : "active" ,
795+ supersededBy : null ,
796+ } ) ;
797+ expect ( deps . targetStore . saveTargetMeta ) . toHaveBeenCalledWith (
798+ expect . any ( String ) ,
799+ supervisor . targetId ,
800+ expect . objectContaining ( {
801+ targetId : supervisor . targetId ,
802+ status : "active" ,
803+ supersededBy : null ,
804+ } )
805+ ) ;
806+ } ) ;
807+
625808 it ( "retries evaluator timeout up to the global retry budget" , async ( ) => {
626809 vi . useFakeTimers ( ) ;
627810 deps . settingsRepo . get = vi . fn ( ( key : string ) => {
@@ -1049,6 +1232,42 @@ describe("SupervisorManager cycle triggers", () => {
10491232 expect ( deps . logger . error ) . not . toHaveBeenCalled ( ) ;
10501233 } ) ;
10511234
1235+ it ( "marks deleted active targets as cancelled with a completion timestamp" , async ( ) => {
1236+ const supervisor = await manager . create ( {
1237+ sessionId : "sess-delete-completed-at" ,
1238+ workspaceId : "ws-1" ,
1239+ objective : "Ship the fix" ,
1240+ evaluatorProviderId : "codex" ,
1241+ } ) ;
1242+
1243+ const activeMeta = {
1244+ targetId : supervisor . targetId ,
1245+ sessionId : supervisor . sessionId ,
1246+ workspaceId : supervisor . workspaceId ,
1247+ objective : supervisor . objective ,
1248+ status : "active" ,
1249+ createdAt : 1 ,
1250+ updatedAt : 1 ,
1251+ supersededBy : null ,
1252+ completedAt : null ,
1253+ } ;
1254+ deps . targetStore . readTargetMeta . mockResolvedValue ( activeMeta ) ;
1255+
1256+ await manager . delete ( supervisor . id ) ;
1257+
1258+ await waitFor ( ( ) => {
1259+ expect ( deps . targetStore . saveTargetMeta ) . toHaveBeenCalledWith (
1260+ expect . any ( String ) ,
1261+ supervisor . targetId ,
1262+ expect . objectContaining ( {
1263+ targetId : supervisor . targetId ,
1264+ status : "cancelled" ,
1265+ completedAt : expect . any ( Number ) ,
1266+ } )
1267+ ) ;
1268+ } ) ;
1269+ } ) ;
1270+
10521271 it ( "pauses an in-flight evaluation by cancelling the cycle" , async ( ) => {
10531272 const supervisor = await manager . create ( {
10541273 sessionId : "sess-pause" ,
0 commit comments