From 3b5137decddbac050543fadb69f8662a1b3dfce7 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:53:57 +0000 Subject: [PATCH 1/3] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/_shared/railway.ts | 67 +++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/supabase/functions/_shared/railway.ts b/supabase/functions/_shared/railway.ts index 8e7a5aa..836aa13 100644 --- a/supabase/functions/_shared/railway.ts +++ b/supabase/functions/_shared/railway.ts @@ -148,18 +148,67 @@ export async function setRailwayReplicas(opts: { serviceInstanceUpdate(serviceId: $serviceId, environmentId: $environmentId, input: $input) } `; - const res = await railwayQuery( - mutation, - { - serviceId: opts.serviceId, - environmentId: opts.environmentId, - input: { numReplicas: opts.replicas }, - }, - opts.token, - ); + // Tenta numReplicas primeiro (formato atual da Railway API v2) + const tryWithKey = async (key: "numReplicas" | "replicas") => { + return await railwayQuery( + mutation, + { + serviceId: opts.serviceId, + environmentId: opts.environmentId, + input: { [key]: opts.replicas }, + }, + opts.token, + ); + }; + + let res = await tryWithKey("numReplicas"); + if (res.errors?.length) { + const msg = JSON.stringify(res.errors); + // Fallback para `replicas` se o schema reclamar do campo + if (/numReplicas/i.test(msg) && /(unknown|not.*found|invalid)/i.test(msg)) { + console.warn("setRailwayReplicas: numReplicas rejected, retrying with `replicas`"); + res = await tryWithKey("replicas"); + } + } if (res.errors?.length) { throw new Error(`setRailwayReplicas failed: ${JSON.stringify(res.errors)}`); } + + // Após escalar, força redeploy para aplicar a mudança imediatamente + try { + await deployRailwayService({ + token: opts.token, + serviceId: opts.serviceId, + environmentId: opts.environmentId, + }); + } catch (e) { + console.warn("setRailwayReplicas: redeploy after scale 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. */ From 858b8f518945616d6e873716bf1b80296a020ad8 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:54:18 +0000 Subject: [PATCH 2/3] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/resume-agent/index.ts | 32 +++++++++++++++-------- supabase/functions/suspend-agent/index.ts | 32 +++++++++++++++-------- 2 files changed, 42 insertions(+), 22 deletions(-) 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) { From a242da381a43cb4cb74be635587e26230ac1e53d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:55:35 +0000 Subject: [PATCH 3/3] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/_shared/railway.ts | 44 ++++++++++++--------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/supabase/functions/_shared/railway.ts b/supabase/functions/_shared/railway.ts index 836aa13..6de6d77 100644 --- a/supabase/functions/_shared/railway.ts +++ b/supabase/functions/_shared/railway.ts @@ -137,44 +137,38 @@ 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) } `; - // Tenta numReplicas primeiro (formato atual da Railway API v2) - const tryWithKey = async (key: "numReplicas" | "replicas") => { - return await railwayQuery( - mutation, - { - serviceId: opts.serviceId, - environmentId: opts.environmentId, - input: { [key]: opts.replicas }, - }, - opts.token, - ); - }; - - let res = await tryWithKey("numReplicas"); + const res = await railwayQuery( + mutation, + { + serviceId: opts.serviceId, + environmentId: opts.environmentId, + input: { sleepApplication: sleep }, + }, + opts.token, + ); if (res.errors?.length) { - const msg = JSON.stringify(res.errors); - // Fallback para `replicas` se o schema reclamar do campo - if (/numReplicas/i.test(msg) && /(unknown|not.*found|invalid)/i.test(msg)) { - console.warn("setRailwayReplicas: numReplicas rejected, retrying with `replicas`"); - res = await tryWithKey("replicas"); - } - } - if (res.errors?.length) { - throw new Error(`setRailwayReplicas failed: ${JSON.stringify(res.errors)}`); + throw new Error(`setRailwayReplicas (sleepApplication=${sleep}) failed: ${JSON.stringify(res.errors)}`); } - // Após escalar, força redeploy para aplicar a mudança imediatamente + // Força redeploy para aplicar imediatamente o novo estado de sleep try { await deployRailwayService({ token: opts.token, @@ -182,7 +176,7 @@ export async function setRailwayReplicas(opts: { environmentId: opts.environmentId, }); } catch (e) { - console.warn("setRailwayReplicas: redeploy after scale failed (non-fatal):", e); + console.warn("setRailwayReplicas: redeploy after sleep change failed (non-fatal):", e); } }