UNPKG

tasker-wrapped-services

Version:

Runtime-agnostic wrapped services for tasker-sequential (Google APIs, keystore, database, etc.)

1,009 lines (859 loc) 56.4 kB
import { config } from "https://deno.land/x/dotenv@v3.2.2/mod.ts"; import "jsr:@supabase/functions-js/edge-runtime.d.ts" import { serve, ConnInfo } from "https://deno.land/std@0.201.0/http/server.ts"; import { corsHeaders } from "../_shared/cors.ts"; // Removed executeTask import - using new synchronous execution model import { jsonResponse, formatTaskResult, formatLogMessage } from "./utils/response-formatter.ts"; import { TaskRegistry } from "./registry/task-registry.ts"; import { createClient, SupabaseClient } from 'https://esm.sh/@supabase/supabase-js@2'; import { generateSchema, formatSchema } from './services/schema-generator.ts'; import { parseJSDocComments } from './utils/jsdoc-parser.ts'; import { GeneratedSchema } from "./types/index.ts"; import { executeMethodChain } from "npm:sdk-http-wrapper@1.0.10/server"; import { hostLog, simpleStringify } from '../_shared/utils.ts'; // Assuming utils are in _shared import { fetchTaskFromDatabase } from "./services/database.ts"; config({ export: true }); declare global { var __updatedFields: Record<string, any>; } // Initialize task registries const basicTaskRegistry = new TaskRegistry(); const specialTaskRegistry = new TaskRegistry(); // Environment setup const extSupabaseUrl = Deno.env.get('EXT_SUPABASE_URL') || ''; const supabaseUrl = Deno.env.get('SUPABASE_URL') || ''; // If the URL is the edge functions URL, use the REST API URL instead for local dev const SUPABASE_URL = extSupabaseUrl.includes('127.0.0.1:8000') ? 'http://localhost:54321' : extSupabaseUrl || (supabaseUrl.includes('127.0.0.1:8000') ? 'http://localhost:54321' : supabaseUrl); const SUPABASE_ANON_KEY = Deno.env.get('EXT_SUPABASE_ANON_KEY') || Deno.env.get('SUPABASE_ANON_KEY') || ''; const SERVICE_ROLE_KEY = Deno.env.get('EXT_SUPABASE_SERVICE_ROLE_KEY') || Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') || ''; console.log(`[INFO] SUPABASE_URL: ${SUPABASE_URL}`); console.log(`[INFO] SERVICE_ROLE_KEY (masked): ${SERVICE_ROLE_KEY ? '*'.repeat(10) : 'MISSING'}`); console.log(`[INFO] Environment variables:`, Deno.env.toObject()); const supabaseClient = createClient(SUPABASE_URL, SERVICE_ROLE_KEY, { auth: { persistSession: false } }); // --- Define the Tasks Service for SDK Wrapper --- const tasksService = { execute: async (taskIdentifier: string, input: Record<string, unknown> = {}, options: { debug?: boolean, verbose?: boolean, include_logs?: boolean } = {}) => { //console.log(`[INFO][SDK Service] Received task execution request for: ${taskIdentifier}`); const logs: string[] = [formatLogMessage('INFO', `[SDK Service] Executing task: ${taskIdentifier}`)]; try { // Check registry first (same logic as direct execution) if (specialTaskRegistry.hasTask(taskIdentifier) || basicTaskRegistry.hasTask(taskIdentifier)) { logs.push(formatLogMessage('INFO', `[SDK Service] Executing registered task: ${taskIdentifier}`)); let result; if (specialTaskRegistry.hasTask(taskIdentifier)) { result = await specialTaskRegistry.executeTask(taskIdentifier, input, logs); } else { result = await basicTaskRegistry.executeTask(taskIdentifier, input, logs); } // The SDK wrapper expects the raw result, not a formatted Response return { success: true, data: result, logs }; } else { // Execute from database using new synchronous execution model logs.push(formatLogMessage('INFO', `[SDK Service] Executing task from database: ${taskIdentifier}`)); // Use internal task execution (same as main handler) const taskFunction = await fetchTaskFromDatabase(undefined, taskIdentifier); if (!taskFunction) { throw new Error(`Task '${taskIdentifier}' not found.`); } // Create task_runs record const taskRunId = crypto.randomUUID(); const baseUrl = Deno.env.get('SUPABASE_URL') || 'http://localhost:54321'; const serviceRoleKey = Deno.env.get('EXT_SUPABASE_SERVICE_ROLE_KEY') || Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') || ''; const taskRunData = { task_function_id: taskFunction.id, task_name: taskFunction.name, input: input || null, status: 'queued' }; const taskRunUrl = `${baseUrl}/rest/v1/task_runs`; const taskRunResponse = await fetch(taskRunUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey, 'Prefer': 'return=representation' }, body: JSON.stringify(taskRunData) }); if (!taskRunResponse.ok) { throw new Error(`Failed to create task run: ${taskRunResponse.status}`); } const taskRunResult = await taskRunResponse.json(); const actualTaskRunId = Array.isArray(taskRunResult) ? taskRunResult[0].id : taskRunResult.id; // Create initial stack_run const stackRunData = { parent_task_run_id: actualTaskRunId, service_name: 'tasks', method_name: 'execute', args: [taskFunction.name, input || null], status: 'pending', vm_state: { taskCode: taskFunction.code, taskName: taskFunction.name, taskInput: input || null } }; const stackRunsUrl = `${baseUrl}/rest/v1/stack_runs`; const stackRunResponse = await fetch(stackRunsUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey, 'Prefer': 'return=representation' }, body: JSON.stringify(stackRunData) }); if (!stackRunResponse.ok) { throw new Error(`Failed to create stack run: ${stackRunResponse.status}`); } const stackRunResult = await stackRunResponse.json(); const stackRunId = Array.isArray(stackRunResult) ? stackRunResult[0].id : stackRunResult.id; // CRITICAL FIX: Always trigger stack processor synchronously for FIFO processing try { const stackProcessorUrl = `${baseUrl}/functions/v1/simple-stack-processor`; const triggerResponse = await fetch(stackProcessorUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${serviceRoleKey}`, }, body: JSON.stringify({ stackRunId }) }); if (triggerResponse.ok) { logs.push(formatLogMessage('INFO', `Stack processor triggered successfully for stack run ${stackRunId}`)); } else { const errorText = await triggerResponse.text(); logs.push(formatLogMessage('ERROR', `Failed to trigger stack processor: ${triggerResponse.status} - ${errorText}`)); } } catch (error) { logs.push(formatLogMessage('ERROR', `Error triggering stack processor: ${error}`)); } // Return success with task run ID for monitoring return { success: true, taskRunId: actualTaskRunId, stackRunId: stackRunId, data: { message: 'Task submitted successfully and will process automatically', taskRunId: actualTaskRunId }, logs }; } } catch (error) { const errorMsg = `[SDK Service] Error executing task ${taskIdentifier}: ${error instanceof Error ? error.message : String(error)}`; console.error(`[ERROR] ${errorMsg}`); logs.push(formatLogMessage('ERROR', errorMsg)); // Throw the error so executeMethodChain can format it throw new Error(errorMsg); } } }; // --------------------------------------------- // Initialize global state if (!globalThis.__updatedFields) globalThis.__updatedFields = {}; function createResponse(data: any, logs: string[] = [], status = 200): Response { return jsonResponse(formatTaskResult(true, data, undefined, logs), status); } function createErrorResponse(errorMessage: string, logs: string[] = [], status = 500): Response { return jsonResponse(formatTaskResult(false, undefined, errorMessage, logs), status); } function createCorsPreflightResponse(): Response { return new Response(null, { status: 204, headers: corsHeaders }); } const CORS_HEADERS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Access-Control-Allow-Methods': 'POST, OPTIONS', }; const LOG_PREFIX_BASE = "[TasksHandlerEF]"; // Tasks Handler Edge Function // Helper function to check if the queue is busy async function checkQueueBusy(baseUrl: string, serviceRoleKey: string): Promise<boolean> { try { // Check for any task_runs or stack_runs that are currently processing const taskRunsUrl = `${baseUrl}/rest/v1/task_runs?status=in.(processing)&limit=1`; const stackRunsUrl = `${baseUrl}/rest/v1/stack_runs?status=in.(processing,pending,pending_resume)&limit=1`; const [taskRunsResponse, stackRunsResponse] = await Promise.all([ fetch(taskRunsUrl, { headers: { 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey } }), fetch(stackRunsUrl, { headers: { 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey } }) ]); if (!taskRunsResponse.ok || !stackRunsResponse.ok) { hostLog(LOG_PREFIX_BASE, 'warn', 'Failed to check queue status, assuming busy'); return true; // Assume busy if we can't check } const taskRuns = await taskRunsResponse.json(); const stackRuns = await stackRunsResponse.json(); const isBusy = (taskRuns.length > 0) || (stackRuns.length > 0); hostLog(LOG_PREFIX_BASE, 'info', `Queue check: ${isBusy ? 'busy' : 'free'} (${taskRuns.length} task_runs, ${stackRuns.length} stack_runs)`); return isBusy; } catch (error) { hostLog(LOG_PREFIX_BASE, 'error', `Error checking queue status: ${error}`); return true; // Assume busy on error } } // Helper function to execute a stack run synchronously async function executeStackRunSynchronously(stackRunId: string, baseUrl: string, serviceRoleKey: string): Promise<{success: boolean, result?: any, error?: string}> { try { hostLog(LOG_PREFIX_BASE, 'info', `Starting synchronous execution of stack run ${stackRunId}`); // Mark task as processing const updateUrl = `${baseUrl}/rest/v1/task_runs?parent_stack_run_id=eq.${stackRunId}`; await fetch(updateUrl, { method: 'PATCH', headers: { 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'processing', updated_at: new Date().toISOString() }) }); // Execute the stack run via simple-stack-processor const stackProcessorUrl = `${baseUrl}/functions/v1/simple-stack-processor`; const response = await fetch(stackProcessorUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${serviceRoleKey}` }, body: JSON.stringify({ stackRunId: stackRunId }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Stack processor failed: ${response.status} - ${errorText}`); } const result = await response.json(); hostLog(LOG_PREFIX_BASE, 'info', `Stack run ${stackRunId} executed with status: ${result.status}`); // After execution, trigger next queued task if any await triggerNextQueuedTask(baseUrl, serviceRoleKey); return { success: result.status === 'completed', result: result.result, error: result.error }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); hostLog(LOG_PREFIX_BASE, 'error', `Synchronous execution failed: ${errorMessage}`); // Try to trigger next queued task even on failure try { await triggerNextQueuedTask(baseUrl, serviceRoleKey); } catch (triggerError) { hostLog(LOG_PREFIX_BASE, 'error', `Failed to trigger next task after error: ${triggerError}`); } return { success: false, error: errorMessage }; } } // FIFO Processing Chain - continuously processes tasks until queue is empty async function triggerFIFOProcessingChain(baseUrl: string, serviceRoleKey: string): Promise<void> { const logPrefix = '[FIFO-Chain]'; hostLog(logPrefix, 'info', 'Starting FIFO processing chain'); let processedCount = 0; const maxProcessingCycles = 100; // Safety limit to prevent infinite loops while (processedCount < maxProcessingCycles) { try { // Find the next queued task or pending stack run (FIFO order) const queuedTasksUrl = `${baseUrl}/rest/v1/task_runs?status=eq.queued&order=created_at.asc&limit=1`; const taskResponse = await fetch(queuedTasksUrl, { headers: { 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey } }); if (!taskResponse.ok) { hostLog(logPrefix, 'warn', 'Failed to check for queued tasks'); break; } const queuedTasks = await taskResponse.json(); // Also check for pending stack runs const pendingStackUrl = `${baseUrl}/rest/v1/stack_runs?status=in.(pending,pending_resume)&order=created_at.asc&limit=1`; const stackResponse = await fetch(pendingStackUrl, { headers: { 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey } }); const pendingStacks = stackResponse.ok ? await stackResponse.json() : []; // If no queued tasks and no pending stacks, we're done if (queuedTasks.length === 0 && pendingStacks.length === 0) { hostLog(logPrefix, 'info', `FIFO processing complete - processed ${processedCount} items`); break; } // Process pending stack runs first (they might be waiting on children) if (pendingStacks.length > 0) { const stackRun = pendingStacks[0]; hostLog(logPrefix, 'info', `Processing pending stack run: ${stackRun.id}`); const stackProcessResponse = await fetch(`${baseUrl}/functions/v1/simple-stack-processor`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${serviceRoleKey}` }, body: JSON.stringify({ stackRunId: stackRun.id }) }); if (stackProcessResponse.ok) { processedCount++; hostLog(logPrefix, 'info', `Stack run ${stackRun.id} processed`); // Brief pause to prevent overwhelming the system await new Promise(resolve => setTimeout(resolve, 100)); continue; } else { hostLog(logPrefix, 'error', `Failed to process stack run ${stackRun.id}`); } } // Process queued tasks if (queuedTasks.length > 0) { const nextTask = queuedTasks[0]; hostLog(logPrefix, 'info', `Processing queued task: ${nextTask.id}`); // Find the associated stack run for this task const taskStackUrl = `${baseUrl}/rest/v1/stack_runs?parent_task_run_id=eq.${nextTask.id}&limit=1`; const taskStackResponse = await fetch(taskStackUrl, { headers: { 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey } }); if (taskStackResponse.ok) { const taskStacks = await taskStackResponse.json(); if (taskStacks.length > 0) { const stackRun = taskStacks[0]; // Trigger simple stack processor for this task const triggerResponse = await fetch(`${baseUrl}/functions/v1/simple-stack-processor`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${serviceRoleKey}` }, body: JSON.stringify({ stackRunId: stackRun.id }) }); if (triggerResponse.ok) { processedCount++; hostLog(logPrefix, 'info', `Task ${nextTask.id} processing initiated`); } else { hostLog(logPrefix, 'error', `Failed to trigger processing for task ${nextTask.id}`); } } } } // Brief pause between processing cycles await new Promise(resolve => setTimeout(resolve, 200)); } catch (error) { hostLog(logPrefix, 'error', `Error in FIFO processing cycle: ${error}`); break; } } if (processedCount >= maxProcessingCycles) { hostLog(logPrefix, 'warn', `FIFO processing stopped after ${processedCount} cycles (safety limit reached)`); } hostLog(logPrefix, 'info', `FIFO processing chain completed - total items processed: ${processedCount}`); } // Helper function to trigger the next queued task async function triggerNextQueuedTask(baseUrl: string, serviceRoleKey: string): Promise<void> { try { // Find the next queued task const queuedTasksUrl = `${baseUrl}/rest/v1/task_runs?status=eq.queued&order=created_at.asc&limit=1`; const response = await fetch(queuedTasksUrl, { headers: { 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey } }); if (!response.ok) { hostLog(LOG_PREFIX_BASE, 'warn', 'Failed to check for queued tasks'); return; } const queuedTasks = await response.json(); if (queuedTasks.length === 0) { hostLog(LOG_PREFIX_BASE, 'info', 'No queued tasks to trigger'); return; } const nextTask = queuedTasks[0]; hostLog(LOG_PREFIX_BASE, 'info', `Triggering next queued task: ${nextTask.id}`); // Find the associated stack run const stackRunUrl = `${baseUrl}/rest/v1/stack_runs?parent_task_run_id=eq.${nextTask.id}&limit=1`; const stackRunResponse = await fetch(stackRunUrl, { headers: { 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey } }); if (stackRunResponse.ok) { const stackRuns = await stackRunResponse.json(); if (stackRuns.length > 0) { const stackRunId = stackRuns[0].id; // Trigger the simple stack processor asynchronously (fire and forget) const stackProcessorUrl = `${baseUrl}/functions/v1/simple-stack-processor`; fetch(stackProcessorUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${serviceRoleKey}` }, body: JSON.stringify({ stackRunId: stackRunId }) }).catch(error => { hostLog(LOG_PREFIX_BASE, 'error', `Failed to trigger next task processor: ${error}`); }); hostLog(LOG_PREFIX_BASE, 'info', `Next task ${nextTask.id} triggered asynchronously`); } } } catch (error) { hostLog(LOG_PREFIX_BASE, 'error', `Error triggering next queued task: ${error}`); } } async function tasksHandler(req: Request): Promise<Response> { // Handle CORS preflight requests if (req.method === "OPTIONS") { return new Response("ok", { headers: CORS_HEADERS }); } let supabaseClient: SupabaseClient; try { if (!SUPABASE_URL || !SERVICE_ROLE_KEY) { hostLog(LOG_PREFIX_BASE, 'error', "Supabase URL or Service Role Key is not configured in environment variables."); throw new Error("Supabase environment variables for service role not set."); } // Initialize Supabase client with service role key for administrative tasks supabaseClient = createClient(SUPABASE_URL, SERVICE_ROLE_KEY, { // No need to pass auth headers explicitly when using service_role_key server-side auth: { persistSession: false } }); } catch (e: unknown) { const error = e instanceof Error ? e : new Error(String(e)); hostLog(LOG_PREFIX_BASE, 'error', "Supabase client (service role) initialization failed:", error.message); return new Response(simpleStringify({ error: "Server configuration error.", details: error.message }), { status: 500, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }); } let requestBody; try { requestBody = await req.json(); } catch (e: unknown) { const error = e instanceof Error ? e : new Error(String(e)); hostLog(LOG_PREFIX_BASE, 'error', "Invalid JSON request body:", error.message); return new Response(simpleStringify({ error: "Invalid JSON request body.", details: error.message }), { status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }); } // Check for special service method calls first if (requestBody.service === "tasks" && requestBody.method === "triggerFIFO") { hostLog(LOG_PREFIX_BASE, 'info', "Received triggerFIFO request, starting FIFO processing chain"); // Start the FIFO processing chain const baseUrl = Deno.env.get('SUPABASE_URL') || 'http://localhost:54321'; const serviceRoleKey = Deno.env.get('EXT_SUPABASE_SERVICE_ROLE_KEY') || Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') || ''; if (!serviceRoleKey) { return new Response(simpleStringify({ error: "Service configuration error" }), { status: 500, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }); } // Don't await - let it run in background triggerFIFOProcessingChain(baseUrl, serviceRoleKey).catch(error => { hostLog(LOG_PREFIX_BASE, 'error', `Error in FIFO processing chain: ${error}`); }); return new Response(simpleStringify({ status: "ok", message: "FIFO processing chain started" }), { status: 200, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }); } const { taskName, input } = requestBody; const logPrefix = `${LOG_PREFIX_BASE} [TaskName: ${taskName || 'N/A'}]`; if (!taskName || typeof taskName !== 'string') { hostLog(logPrefix, 'error', "'taskName' is required in the request body and must be a string."); return new Response(simpleStringify({ error: "'taskName' is required and must be a string." }), { status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }); } try { // Step 1: Fetch Task Definition from task_functions table hostLog(logPrefix, 'info', `Attempting to fetch definition for task: '${taskName}'`); // Use fetchTaskFromDatabase which now has direct HTTP fetch as a fallback const taskFunction = await fetchTaskFromDatabase(undefined, taskName); if (!taskFunction) { hostLog(logPrefix, 'warn', `Task definition not found for '${taskName}'.`); return new Response(simpleStringify({ error: `Task '${taskName}' not found.` }), { status: 404, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }); } hostLog(logPrefix, 'info', `Task definition '${taskFunction.name}' (ID: ${taskFunction.id}) found.`); // Step 2: Create a task_runs record to track the overall user request hostLog(logPrefix, 'info', `Creating task_run record with input:`, input || '(no input)'); // Use direct fetch method for creating task_run record const baseUrl = Deno.env.get('SUPABASE_URL') || 'http://localhost:54321'; const serviceRoleKey = Deno.env.get('EXT_SUPABASE_SERVICE_ROLE_KEY') || Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') || ''; if (!serviceRoleKey) { throw new Error("Service role key not available for direct insert"); } const taskRunData = { task_function_id: taskFunction.id, task_name: taskFunction.name, input: input || null, status: 'queued' }; const url = `${baseUrl}/rest/v1/task_runs`; const insertResponse = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey, 'Prefer': 'return=representation' }, body: JSON.stringify(taskRunData) }); if (!insertResponse.ok) { const errorText = await insertResponse.text(); hostLog(logPrefix, 'error', `Failed to create task_run record in database: HTTP ${insertResponse.status} ${insertResponse.statusText}`, errorText); throw new Error(`Database error: Failed to initiate task run. HTTP ${insertResponse.status} ${insertResponse.statusText}`); } const taskRunResult = await insertResponse.json(); const taskRunId = Array.isArray(taskRunResult) && taskRunResult.length > 0 ? taskRunResult[0].id : null; if (!taskRunId) { hostLog(logPrefix, 'error', "Failed to obtain task_run ID after insertion"); throw new Error("Database error: Failed to obtain task run ID after insertion"); } hostLog(logPrefix, 'info', `Task_run record created successfully: ${taskRunId}`); // Step 3: Skip queue busy check - always process immediately with stack processor // Step 4: Create the initial stack_runs record to kick off the execution hostLog(logPrefix, 'info', `Creating initial stack_run for task_run ${taskRunId}`); // Use direct fetch for creating stack_run record - call deno-executor directly const stackRunData = { parent_task_run_id: taskRunId, service_name: 'deno-executor', method_name: 'execute', args: [taskFunction.name, input || null], status: 'pending', vm_state: { taskCode: taskFunction.code, taskName: taskFunction.name, taskInput: input || null } }; const stackRunsUrl = `${baseUrl}/rest/v1/stack_runs`; const stackRunResponse = await fetch(stackRunsUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey, 'Prefer': 'return=representation' }, body: JSON.stringify(stackRunData) }); if (!stackRunResponse.ok) { const errorText = await stackRunResponse.text(); hostLog(logPrefix, 'error', `Failed to create initial stack_run record in database: HTTP ${stackRunResponse.status} ${stackRunResponse.statusText}`, errorText); // Attempt to mark the parent task_run as failed to avoid orphaned task_runs try { const updateTaskRunUrl = `${baseUrl}/rest/v1/task_runs?id=eq.${encodeURIComponent(taskRunId)}`; await fetch(updateTaskRunUrl, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${serviceRoleKey}`, 'apikey': serviceRoleKey }, body: JSON.stringify({ status: 'failed', error: { message: "System error: Failed to create initial stack_run for task execution.", details: `HTTP ${stackRunResponse.status} ${stackRunResponse.statusText}` }, ended_at: new Date().toISOString() }) }); } catch (updateErr) { hostLog(logPrefix, 'error', "Additionally failed to mark task_run as failed:", updateErr); } throw new Error(`Database error: Failed to create initial stack run. HTTP ${stackRunResponse.status} ${stackRunResponse.statusText}`); } const stackRunResult = await stackRunResponse.json(); const stackRunId = Array.isArray(stackRunResult) && stackRunResult.length > 0 ? stackRunResult[0].id : null; if (!stackRunId) { hostLog(logPrefix, 'error', "Failed to obtain stack_run ID after insertion"); throw new Error("Database error: Failed to obtain stack run ID after insertion"); } hostLog(logPrefix, 'info', `Initial stack_run ${stackRunId} created. Task '${taskName}' (run ID: ${taskRunId}) has been successfully offloaded.`); // Step 4: No longer pre-triggering stack processor - will be handled in Step 5 // Step 5: Always trigger the simple stack processor for automatic FIFO processing hostLog(logPrefix, 'info', `Always triggering simple stack processor for automatic processing of stack run ${stackRunId}`); // CRITICAL: Use fire-and-forget pattern to avoid blocking const stackProcessorUrl = `${baseUrl}/functions/v1/simple-stack-processor`; hostLog(logPrefix, 'info', `Triggering stack processor for automatic processing (async)`); // Fire-and-forget - don't wait for response setTimeout(() => { fetch(stackProcessorUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${serviceRoleKey}`, }, body: JSON.stringify({ trigger: 'process-next' }) }).catch(error => { hostLog(logPrefix, 'warn', `Async stack processor trigger failed (non-critical): ${error}`); }); }, 0); // Return immediately - processing will continue automatically return new Response(simpleStringify({ message: "Task submitted successfully and will process automatically.", taskRunId: taskRunId, stackRunId: stackRunId, status: "submitted", info: "Processing will continue automatically until completion - no manual triggers needed" }), { status: 202, // HTTP 202 Accepted: Processing started headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }); } catch (e: unknown) { const error = e instanceof Error ? e : new Error(String(e)); hostLog(logPrefix, 'error', "Unhandled error in /tasks endpoint handler:", error.message, error.stack); // Avoid exposing detailed internal errors to the client unless necessary return new Response(simpleStringify({ error: "An unexpected server error occurred while processing the task request." }), { status: 500, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }); } } // New handler for getting task status async function statusHandler(req: Request): Promise<Response> { // Extract taskRunId from query params in URL for GET requests const url = new URL(req.url); const taskRunId = url.searchParams.get('id'); const logPrefix = `[tasks/status/${taskRunId}]`; hostLog(logPrefix, 'info', `Received status request for task run ID: ${taskRunId}`); if (!taskRunId) { return new Response( simpleStringify({ error: 'Missing taskRunId parameter' }), { status: 400, headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' } } ); } try { // Determine the correct baseUrl for database access const extSupabaseUrl = Deno.env.get("EXT_SUPABASE_URL") || ""; const supabaseUrl = Deno.env.get("SUPABASE_URL") || "http://kong:8000"; // Use Kong URL for local development or when EXT_SUPABASE_URL is missing const useKong = extSupabaseUrl.includes('localhost') || extSupabaseUrl.includes('127.0.0.1') || !extSupabaseUrl; const baseUrl = useKong ? 'http://kong:8000/rest/v1' : `${SUPABASE_URL}/rest/v1`; // Fetch task run from database const dbUrl = `${baseUrl}/task_runs?id=eq.${taskRunId}&select=*`; hostLog(logPrefix, 'info', `Attempting to fetch task run from: ${dbUrl}`); const response = await fetch(dbUrl, { headers: { 'Authorization': `Bearer ${SERVICE_ROLE_KEY}`, 'apikey': SERVICE_ROLE_KEY } }); if (!response.ok) { const error = await response.text(); const errorMessage = `Database query failed: ${error}`; hostLog(logPrefix, 'error', errorMessage); return new Response( simpleStringify({ error: `Failed to fetch task status: ${errorMessage}` }), { status: 500, headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' } } ); } const tasks = await response.json(); hostLog(logPrefix, 'info', `Database query successful. Found ${tasks.length} records.`); if (tasks.length === 0) { return new Response( simpleStringify({ error: `Task run with ID ${taskRunId} not found` }), { status: 404, headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' } } ); } const taskRun = tasks[0]; // Check if task has been stuck in 'queued' or 'processing' state if ((taskRun.status === 'queued' || taskRun.status === 'processing') && taskRun.created_at) { const createdAt = new Date(taskRun.created_at); const now = new Date(); const diffInSeconds = Math.floor((now.getTime() - createdAt.getTime()) / 1000); // Even for recent tasks, do a quick check for completed results hostLog(logPrefix, 'info', `Task is in ${taskRun.status} state for ${diffInSeconds} seconds, checking if there's a completed stack run`); // First try: Get the stack run associated with this task try { // Use parent_task_run_id field const stackRunUrl = `${baseUrl}/stack_runs?select=*&parent_task_run_id=eq.${taskRunId}&status=eq.completed&order=created_at.desc&limit=1`; const stackRunResponse = await fetch(stackRunUrl, { headers: { 'Authorization': `Bearer ${SERVICE_ROLE_KEY}`, 'Content-Type': 'application/json' } }); if (stackRunResponse.ok) { const stackRuns = await stackRunResponse.json(); if (stackRuns && stackRuns.length > 0) { const completedStackRun = stackRuns[0]; hostLog(logPrefix, 'warn', `Found completed stack run ${completedStackRun.id} with result, updating task run status`); // Update the task run to completed with the result from the stack run const updateUrl = `${baseUrl}/task_runs?id=eq.${taskRunId}`; const updateResponse = await fetch(updateUrl, { method: 'PATCH', headers: { 'Authorization': `Bearer ${SERVICE_ROLE_KEY}`, 'apikey': SERVICE_ROLE_KEY, 'Content-Type': 'application/json', 'Prefer': 'return=minimal' }, body: JSON.stringify({ status: 'completed', result: completedStackRun.result, updated_at: new Date().toISOString(), ended_at: new Date().toISOString() }) }); if (!updateResponse.ok) { const updateError = await updateResponse.text(); hostLog(logPrefix, 'error', `Failed to update task run status: ${updateError}`); } else { hostLog(logPrefix, 'info', `Successfully updated task run ${taskRunId} to completed status`); // Update the taskRun object with the new status and result taskRun.status = 'completed'; taskRun.result = completedStackRun.result; taskRun.updated_at = new Date().toISOString(); taskRun.ended_at = new Date().toISOString(); } } else { // Try a different query using parent_task_run_id field (handles legacy/compatibility) try { const altStackRunUrl = `${baseUrl}/stack_runs?select=*&parent_task_run_id=eq.${taskRunId}&status=eq.completed&order=created_at.desc&limit=1`; const altStackRunResponse = await fetch(altStackRunUrl, { headers: { 'Authorization': `Bearer ${SERVICE_ROLE_KEY}`, 'apikey': SERVICE_ROLE_KEY } }); if (altStackRunResponse.ok) { const altStackRuns = await altStackRunResponse.json(); if (altStackRuns && altStackRuns.length > 0) { const completedStackRun = altStackRuns[0]; hostLog(logPrefix, 'warn', `Found completed stack run ${completedStackRun.id} with result (via parent_task_run_id), updating task run status`); // Update the task run to completed with the result from the stack run const updateUrl = `${baseUrl}/task_runs?id=eq.${taskRunId}`; const updateResponse = await fetch(updateUrl, { method: 'PATCH', headers: { 'Authorization': `Bearer ${SERVICE_ROLE_KEY}`, 'apikey': SERVICE_ROLE_KEY, 'Content-Type': 'application/json', 'Prefer': 'return=minimal' }, body: JSON.stringify({ status: 'completed', result: completedStackRun.result, updated_at: new Date().toISOString(), ended_at: new Date().toISOString() }) }); if (updateResponse.ok) { hostLog(logPrefix, 'info', `Successfully updated task run ${taskRunId} to completed status (via parent_task_run_id)`); // Update the taskRun object taskRun.status = 'completed'; taskRun.result = completedStackRun.result; taskRun.updated_at = new Date().toISOString(); taskRun.ended_at = new Date().toISOString(); } } else { hostLog(logPrefix, 'info', `No completed stack run found for task run ${taskRunId} with either query method`); // For long-running tasks, check if we need a manual cleanup if (diffInSeconds > 60) { hostLog(logPrefix, 'warn', `Task has been stuck in ${taskRun.status} state for over 60 seconds, performing manual check`); // Fallback: Try to find any stack run related to this task try { if (taskRun.waiting_on_stack_run_id) { // If we have a waiting_on_stack_run_id, just wait for that to complete hostLog(logPrefix, 'info', `Task is waiting on stack run ${taskRun.waiting_on_stack_run_id}, no manual intervention needed`); // Continue with normal status return } const allStackRunsUrl = `${baseUrl}/stack_runs?select=*&or=(parent_task_run_id.eq.${taskRunId})&order=created_at.desc&limit=1`; hostLog(logPrefix, 'info', `Checking for any stack runs: ${allStackRunsUrl}`); const allStackRunsResponse = await fetch(allStackRunsUrl, { headers: { 'Authorization': `Bearer ${SERVICE_ROLE_KEY}`, 'Content-Type': 'application/json' } }); if (allStackRunsResponse.ok) { const allStackRuns = await allStackRunsResponse.json(); if (allStackRuns && allStackRuns.length > 0) { const latestStackRun = allStackRuns[0]; hostLog(logPrefix, 'warn', `Found stack run ${latestStackRun.id} with status ${latestStackRun.status}, but no completed result`); // If task has been running too long, mark as error if (diffInSeconds > 120) { const updateUrl = `${baseUrl}/task_runs?id=eq.${taskRunId}`; await fetch(updateUrl, { method: 'PATCH', headers: { 'Authorization': `Bearer ${SERVICE_ROLE_KEY}`, 'apikey': SERVICE_ROLE_KEY, 'Content-Type': 'application/json', 'Prefer': 'return=minimal' }, body: JSON.stringify({ status: 'error', error: { message: 'Task execution timed out after 120 seconds' }, updated_at: new Date().toISOString(), ended_at: new Date().toISOString() }) }); taskRun.status = 'error'; taskRun.error = { message: 'Task execution timed out after 120 seconds' }; taskRun.updated_at = new Date().toISOString(); taskRun.ended_at = new Date().toISOString(); } } } } catch (e) { hostLog(logPrefix, 'error', `Error checking for completed stack runs: ${e instanceof Error ? e.message : String(e)}`); } } } } } catch (altError) { hostLog(logPrefix, 'error', `Error checking alternative stack runs query: ${altError instanceof Error ? altError.message : String(altError)}`); } } } else { const stackRunError = await stackRunResponse.text(); hostLog(logPrefix, 'error', `Failed to check for completed stack runs: ${stackRunError}`); } } catch (e) { hostLog(logPrefix, 'error', `Error checking for completed stack runs: ${e instanceof Error ? e.message : String(e)}`); } } hostLog(logPrefix, 'info', `Returning task run with status: ${taskRun.status}`); // Extra handling for error states to make debugging easier if (taskRun.status === 'error') { hostLog(logPrefix, 'warn', `Task in error state. Error details: ${JSON.stringify(taskRun.error || 'No error details available')}`); } return new Response( simpleStringify(taskRun), { status: 200, headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' } } ); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); hostLog(logPrefix, 'error', `Exception in statusHandler: ${errorMessage}`);