@@ -17,7 +17,7 @@ import { writeFile } from 'fs/promises';
1717import { existsSync } from 'fs' ;
1818import { join } from 'path' ;
1919import { cosEvents , emitLog } from './cos.js' ;
20- import { DAY , ensureDir , HOUR , readJSONFile , PATHS } from '../lib/fileUtils.js' ;
20+ import { DAY , ensureDir , HOUR , readJSONFile , PATHS , safeDate } from '../lib/fileUtils.js' ;
2121import { getAdaptiveCooldownMultiplier } from './taskLearning.js' ;
2222import { isTaskTypeEnabledForApp , getAppTaskTypeInterval , getActiveApps , getAppTaskTypeOverrides } from './apps.js' ;
2323import { PORTOS_UI_URL } from '../lib/ports.js' ;
@@ -1174,6 +1174,34 @@ export async function getExecutionHistory(taskType) {
11741174 return schedule . executions [ key ] || { lastRun : null , count : 0 , perApp : { } } ;
11751175}
11761176
1177+ /**
1178+ * Check if all runAfter dependencies have completed since this task's last run.
1179+ * Returns { satisfied, pending } where pending lists unfinished dependency task types.
1180+ */
1181+ function checkRunAfterDeps ( schedule , taskType , appId = null ) {
1182+ const interval = schedule . tasks [ taskType ] ;
1183+ const deps = interval ?. runAfter ;
1184+ if ( ! deps || deps . length === 0 ) return { satisfied : true , pending : [ ] } ;
1185+
1186+ const key = `task:${ taskType } ` ;
1187+ const execution = schedule . executions [ key ] || { lastRun : null , perApp : { } } ;
1188+ const ownLastRun = safeDate ( appId ? execution . perApp [ appId ] ?. lastRun : execution . lastRun ) ;
1189+
1190+ const pending = [ ] ;
1191+ for ( const dep of deps ) {
1192+ const depKey = `task:${ dep } ` ;
1193+ const depExec = schedule . executions [ depKey ] || { lastRun : null , perApp : { } } ;
1194+ const depLastRun = safeDate ( appId ? depExec . perApp [ appId ] ?. lastRun : depExec . lastRun ) ;
1195+
1196+ // Dependency must have run after this task's last run (i.e., within the current cycle)
1197+ if ( depLastRun <= ownLastRun ) {
1198+ pending . push ( dep ) ;
1199+ }
1200+ }
1201+
1202+ return { satisfied : pending . length === 0 , pending } ;
1203+ }
1204+
11771205/**
11781206 * Check if a task type should run for a specific app (or globally)
11791207 */
@@ -1227,71 +1255,82 @@ export async function shouldRunTask(taskType, appId = null) {
12271255 return result ;
12281256 } ;
12291257
1258+ let result ;
1259+
12301260 switch ( effectiveType ) {
12311261 case INTERVAL_TYPES . ROTATION :
1232- return { shouldRun : true , reason : 'rotation' } ;
1262+ result = { shouldRun : true , reason : 'rotation' } ;
1263+ break ;
12331264
12341265 case INTERVAL_TYPES . DAILY : {
12351266 const learningAdjustment = await getPerformanceAdjustedInterval ( taskType , DAY ) ;
12361267 const adjustedInterval = learningAdjustment . adjustedIntervalMs ;
1237-
12381268 if ( timeSinceLastRun >= adjustedInterval ) {
1239- return buildResult ( true , learningAdjustment . adjusted ? 'daily-due-adjusted' : 'daily-due' , DAY , { learningAdjustment } ) ;
1269+ result = buildResult ( true , learningAdjustment . adjusted ? 'daily-due-adjusted' : 'daily-due' , DAY , { learningAdjustment } ) ;
1270+ } else {
1271+ result = buildResult ( false , learningAdjustment . adjusted ? 'daily-cooldown-adjusted' : 'daily-cooldown' , DAY , {
1272+ learningAdjustment, nextRunIn : adjustedInterval - timeSinceLastRun ,
1273+ nextRunAt : new Date ( lastRun + adjustedInterval ) . toISOString ( ) ,
1274+ baseIntervalMs : DAY , adjustedIntervalMs : adjustedInterval
1275+ } ) ;
12401276 }
1241- return buildResult ( false , learningAdjustment . adjusted ? 'daily-cooldown-adjusted' : 'daily-cooldown' , DAY , {
1242- learningAdjustment,
1243- nextRunIn : adjustedInterval - timeSinceLastRun ,
1244- nextRunAt : new Date ( lastRun + adjustedInterval ) . toISOString ( ) ,
1245- baseIntervalMs : DAY ,
1246- adjustedIntervalMs : adjustedInterval
1247- } ) ;
1277+ break ;
12481278 }
12491279
12501280 case INTERVAL_TYPES . WEEKLY : {
12511281 const learningAdjustment = await getPerformanceAdjustedInterval ( taskType , WEEK ) ;
12521282 const adjustedInterval = learningAdjustment . adjustedIntervalMs ;
1253-
12541283 if ( timeSinceLastRun >= adjustedInterval ) {
1255- return buildResult ( true , learningAdjustment . adjusted ? 'weekly-due-adjusted' : 'weekly-due' , WEEK , { learningAdjustment } ) ;
1284+ result = buildResult ( true , learningAdjustment . adjusted ? 'weekly-due-adjusted' : 'weekly-due' , WEEK , { learningAdjustment } ) ;
1285+ } else {
1286+ result = buildResult ( false , learningAdjustment . adjusted ? 'weekly-cooldown-adjusted' : 'weekly-cooldown' , WEEK , {
1287+ learningAdjustment, nextRunIn : adjustedInterval - timeSinceLastRun ,
1288+ nextRunAt : new Date ( lastRun + adjustedInterval ) . toISOString ( ) ,
1289+ baseIntervalMs : WEEK , adjustedIntervalMs : adjustedInterval
1290+ } ) ;
12561291 }
1257- return buildResult ( false , learningAdjustment . adjusted ? 'weekly-cooldown-adjusted' : 'weekly-cooldown' , WEEK , {
1258- learningAdjustment,
1259- nextRunIn : adjustedInterval - timeSinceLastRun ,
1260- nextRunAt : new Date ( lastRun + adjustedInterval ) . toISOString ( ) ,
1261- baseIntervalMs : WEEK ,
1262- adjustedIntervalMs : adjustedInterval
1263- } ) ;
1292+ break ;
12641293 }
12651294
12661295 case INTERVAL_TYPES . ONCE :
1267- if ( appExecution . count === 0 ) {
1268- return { shouldRun : true , reason : 'once-first-run' } ;
1269- }
1270- return { shouldRun : false , reason : 'once-completed' , completedAt : appExecution . lastRun } ;
1296+ result = appExecution . count === 0
1297+ ? { shouldRun : true , reason : 'once-first-run' }
1298+ : { shouldRun : false , reason : 'once-completed' , completedAt : appExecution . lastRun } ;
1299+ break ;
12711300
12721301 case INTERVAL_TYPES . ON_DEMAND :
1273- return { shouldRun : false , reason : 'on-demand-only' } ;
1302+ result = { shouldRun : false , reason : 'on-demand-only' } ;
1303+ break ;
12741304
12751305 case INTERVAL_TYPES . CUSTOM : {
12761306 const baseInterval = interval . intervalMs || DAY ;
12771307 const learningAdjustment = await getPerformanceAdjustedInterval ( taskType , baseInterval ) ;
12781308 const adjustedInterval = learningAdjustment . adjustedIntervalMs ;
1279-
12801309 if ( timeSinceLastRun >= adjustedInterval ) {
1281- return buildResult ( true , learningAdjustment . adjusted ? 'custom-due-adjusted' : 'custom-due' , baseInterval , { learningAdjustment } ) ;
1310+ result = buildResult ( true , learningAdjustment . adjusted ? 'custom-due-adjusted' : 'custom-due' , baseInterval , { learningAdjustment } ) ;
1311+ } else {
1312+ result = buildResult ( false , learningAdjustment . adjusted ? 'custom-cooldown-adjusted' : 'custom-cooldown' , baseInterval , {
1313+ learningAdjustment, nextRunIn : adjustedInterval - timeSinceLastRun ,
1314+ nextRunAt : new Date ( lastRun + adjustedInterval ) . toISOString ( ) ,
1315+ baseIntervalMs : baseInterval , adjustedIntervalMs : adjustedInterval
1316+ } ) ;
12821317 }
1283- return buildResult ( false , learningAdjustment . adjusted ? 'custom-cooldown-adjusted' : 'custom-cooldown' , baseInterval , {
1284- learningAdjustment,
1285- nextRunIn : adjustedInterval - timeSinceLastRun ,
1286- nextRunAt : new Date ( lastRun + adjustedInterval ) . toISOString ( ) ,
1287- baseIntervalMs : baseInterval ,
1288- adjustedIntervalMs : adjustedInterval
1289- } ) ;
1318+ break ;
12901319 }
12911320
12921321 default :
1293- return { shouldRun : true , reason : 'unknown-default-rotation' } ;
1322+ result = { shouldRun : true , reason : 'unknown-default-rotation' } ;
12941323 }
1324+
1325+ // If the task would run, check runAfter dependencies — blocked until all deps have run since our last run
1326+ if ( result . shouldRun && interval . runAfter ?. length > 0 ) {
1327+ const depCheck = checkRunAfterDeps ( schedule , taskType , appId ) ;
1328+ if ( ! depCheck . satisfied ) {
1329+ return { shouldRun : false , reason : 'waiting-on-dependencies' , pendingDeps : depCheck . pending } ;
1330+ }
1331+ }
1332+
1333+ return result ;
12951334}
12961335
12971336/**
0 commit comments