diff --git a/supabase/functions/_shared/railway.ts b/supabase/functions/_shared/railway.ts index 8e7a5aa..6de6d77 100644 --- a/supabase/functions/_shared/railway.ts +++ b/supabase/functions/_shared/railway.ts @@ -137,12 +137,19 @@ export async function deployRailwayService(opts: { } } +/** + * Suspende ou retoma um serviço Railway usando `sleepApplication`. + * Railway não aceita `numReplicas: 0` via serviceInstanceUpdate — o caminho + * suportado para pausar é `sleepApplication: true` (e `false` para acordar). + * `replicas <= 0` => sleep, `>= 1` => wake. + */ export async function setRailwayReplicas(opts: { token: string; serviceId: string; environmentId: string; replicas: number; }): Promise { + const sleep = opts.replicas <= 0; const mutation = ` mutation ServiceInstanceUpdate($serviceId: String!, $environmentId: String!, $input: ServiceInstanceUpdateInput!) { serviceInstanceUpdate(serviceId: $serviceId, environmentId: $environmentId, input: $input) @@ -153,13 +160,49 @@ export async function setRailwayReplicas(opts: { { serviceId: opts.serviceId, environmentId: opts.environmentId, - input: { numReplicas: opts.replicas }, + input: { sleepApplication: sleep }, }, opts.token, ); if (res.errors?.length) { - throw new Error(`setRailwayReplicas failed: ${JSON.stringify(res.errors)}`); + throw new Error(`setRailwayReplicas (sleepApplication=${sleep}) failed: ${JSON.stringify(res.errors)}`); } + + // Força redeploy para aplicar imediatamente o novo estado de sleep + try { + await deployRailwayService({ + token: opts.token, + serviceId: opts.serviceId, + environmentId: opts.environmentId, + }); + } catch (e) { + console.warn("setRailwayReplicas: redeploy after sleep change failed (non-fatal):", e); + } +} + +/** Busca o environmentId do primeiro deployment de um serviço. Útil quando vps_pool_id está null. */ +export async function getServiceEnvironmentId(opts: { + token: string; + serviceId: string; +}): Promise { + const query = ` + query Service($id: String!) { + service(id: $id) { + projectId + deployments(first: 1) { + edges { node { environmentId } } + } + } + } + `; + const res = await railwayQuery<{ + service: { projectId: string; deployments: { edges: { node: { environmentId: string } }[] } }; + }>(query, { id: opts.serviceId }, opts.token); + if (res.errors?.length) { + console.error("getServiceEnvironmentId errors:", JSON.stringify(res.errors)); + return null; + } + return res.data?.service?.deployments?.edges?.[0]?.node?.environmentId ?? null; } /** Apaga o webhook do Telegram para que o Hermes assuma via polling. */ diff --git a/supabase/functions/resume-agent/index.ts b/supabase/functions/resume-agent/index.ts index 525a208..a50c098 100644 --- a/supabase/functions/resume-agent/index.ts +++ b/supabase/functions/resume-agent/index.ts @@ -5,7 +5,7 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; import { corsHeaders } from "../_shared/cors.ts"; -import { setRailwayReplicas } from "../_shared/railway.ts"; +import { setRailwayReplicas, getServiceEnvironmentId } from "../_shared/railway.ts"; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; @@ -45,7 +45,7 @@ Deno.serve(async (req) => { if (!agent) return jsonResponse(404, { error: "agent_instance not found" }); - if (!agent.railway_service_id || !agent.vps_pool_id) { + if (!agent.railway_service_id) { return jsonResponse(409, { error: "agent has no container — needs full provisioning instead", }); @@ -55,21 +55,31 @@ Deno.serve(async (req) => { return jsonResponse(200, { ok: true, already_active: true }); } - const { data: pool } = await supabase - .from("vps_pool") - .select("railway_environment_id") - .eq("id", agent.vps_pool_id) - .maybeSingle(); - - if (!pool?.railway_environment_id) { - return jsonResponse(500, { error: "pool environment not configured" }); + // Resolve environmentId: prioriza vps_pool, fallback para query no Railway + let environmentId: string | null = null; + if (agent.vps_pool_id) { + const { data: pool } = await supabase + .from("vps_pool") + .select("railway_environment_id") + .eq("id", agent.vps_pool_id) + .maybeSingle(); + environmentId = pool?.railway_environment_id ?? null; + } + if (!environmentId) { + environmentId = await getServiceEnvironmentId({ + token: RAILWAY_API_TOKEN, + serviceId: agent.railway_service_id, + }); + } + if (!environmentId) { + return jsonResponse(500, { error: "could not resolve railway environmentId" }); } try { await setRailwayReplicas({ token: RAILWAY_API_TOKEN, serviceId: agent.railway_service_id, - environmentId: pool.railway_environment_id, + environmentId, replicas: 1, }); } catch (e) { diff --git a/supabase/functions/suspend-agent/index.ts b/supabase/functions/suspend-agent/index.ts index f81837b..d53c774 100644 --- a/supabase/functions/suspend-agent/index.ts +++ b/supabase/functions/suspend-agent/index.ts @@ -5,7 +5,7 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; import { corsHeaders } from "../_shared/cors.ts"; -import { setRailwayReplicas } from "../_shared/railway.ts"; +import { setRailwayReplicas, getServiceEnvironmentId } from "../_shared/railway.ts"; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; @@ -47,7 +47,7 @@ Deno.serve(async (req) => { if (agent.status === "suspended") { return jsonResponse(200, { ok: true, already_suspended: true }); } - if (!agent.railway_service_id || !agent.vps_pool_id) { + if (!agent.railway_service_id) { // Sem container provisionado ainda — só marca o status await supabase .from("agent_instances") @@ -56,21 +56,31 @@ Deno.serve(async (req) => { return jsonResponse(200, { ok: true, no_container: true }); } - const { data: pool } = await supabase - .from("vps_pool") - .select("railway_environment_id") - .eq("id", agent.vps_pool_id) - .maybeSingle(); - - if (!pool?.railway_environment_id) { - return jsonResponse(500, { error: "pool environment not configured" }); + // Resolve o environmentId: prioriza vps_pool, fallback para query no Railway + let environmentId: string | null = null; + if (agent.vps_pool_id) { + const { data: pool } = await supabase + .from("vps_pool") + .select("railway_environment_id") + .eq("id", agent.vps_pool_id) + .maybeSingle(); + environmentId = pool?.railway_environment_id ?? null; + } + if (!environmentId) { + environmentId = await getServiceEnvironmentId({ + token: RAILWAY_API_TOKEN, + serviceId: agent.railway_service_id, + }); + } + if (!environmentId) { + return jsonResponse(500, { error: "could not resolve railway environmentId" }); } try { await setRailwayReplicas({ token: RAILWAY_API_TOKEN, serviceId: agent.railway_service_id, - environmentId: pool.railway_environment_id, + environmentId, replicas: 0, }); } catch (e) {