diff --git a/desktop/src/features/agents/lib/managedAgentControlActions.test.mjs b/desktop/src/features/agents/lib/managedAgentControlActions.test.mjs index 020c02d9d..81899aa4e 100644 --- a/desktop/src/features/agents/lib/managedAgentControlActions.test.mjs +++ b/desktop/src/features/agents/lib/managedAgentControlActions.test.mjs @@ -39,23 +39,33 @@ function agent(overrides = {}) { }; } -test("relay-mesh agents cannot be manually started without a fresh target", async () => { - let called = false; +test("relay-mesh agents delegate start to the backend preflight", async () => { + const meshAgent = agent({ + envVars: { + SPROUT_AGENT_PROVIDER: "openai", + OPENAI_COMPAT_BASE_URL: "http://127.0.0.1:9337/v1/", + }, + }); + + let calledWith = null; + await startManagedAgentWithRules({ + agent: meshAgent, + startManagedAgent: async (pubkey) => { + calledWith = pubkey; + }, + }); + assert.equal(calledWith, meshAgent.pubkey); + + // Backend preflight failures (e.g. no live serve target) propagate as-is. await assert.rejects( startManagedAgentWithRules({ - agent: agent({ - envVars: { - SPROUT_AGENT_PROVIDER: "openai", - OPENAI_COMPAT_BASE_URL: "http://127.0.0.1:9337/v1/", - }, - }), + agent: meshAgent, startManagedAgent: async () => { - called = true; + throw new Error("no live serve target is available for this model"); }, }), - /Relay-mesh agents need a fresh serve target/, + /no live serve target/, ); - assert.equal(called, false); }); test("ordinary local agents still start normally", async () => { diff --git a/desktop/src/features/agents/lib/managedAgentControlActions.ts b/desktop/src/features/agents/lib/managedAgentControlActions.ts index 9b658c22a..0bf30b70d 100644 --- a/desktop/src/features/agents/lib/managedAgentControlActions.ts +++ b/desktop/src/features/agents/lib/managedAgentControlActions.ts @@ -75,18 +75,6 @@ export function resolveManagedAgentChannelId( return matches.length === 1 ? matches[0].id : null; } -function relayMeshAgentError(agent: ManagedAgent): string | null { - if (agent.backend.type !== "local") return null; - if (agent.envVars.SPROUT_AGENT_PROVIDER !== "openai") return null; - if ( - agent.envVars.OPENAI_COMPAT_BASE_URL?.replace(/\/+$/, "") !== - "http://127.0.0.1:9337/v1" - ) { - return null; - } - return "Relay-mesh agents need a fresh serve target before start. Create a new agent with Run on relay mesh selected."; -} - export async function startManagedAgentWithRules({ agent, startManagedAgent, @@ -94,8 +82,9 @@ export async function startManagedAgentWithRules({ agent: ManagedAgent; startManagedAgent: StartManagedAgent; }) { - const relayMeshError = relayMeshAgentError(agent); - if (relayMeshError) throw new Error(relayMeshError); + // Relay-mesh agents are no longer blocked here: the backend start preflight + // (ensure_relay_mesh_for_record) re-resolves a live serve target and dials + // it, failing with an actionable error when no peer serves the model. await startManagedAgent(agent.pubkey); } @@ -108,8 +97,6 @@ export async function respawnManagedAgentWithRules({ startManagedAgent: StartManagedAgent; stopManagedAgent: StopManagedAgent; }) { - const relayMeshError = relayMeshAgentError(agent); - if (relayMeshError) throw new Error(relayMeshError); if (agent.backend.type === "local" && isManagedAgentActive(agent)) { await stopManagedAgent(agent.pubkey); } diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index f851ab514..7414a9715 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -4765,9 +4765,18 @@ async function handleStartManagedAgent(args: { }): Promise { const agent = getMockManagedAgent(args.pubkey); if (isRelayMeshManagedAgent(agent)) { - throw new Error( - "relay mesh agents cannot be started from saved state because the selected serve target is not persisted. Create a new agent with Run on relay mesh selected to refresh the target for http://127.0.0.1:9337/v1.", - ); + // Model the backend start preflight (ensure_relay_mesh_for_record): a + // saved relay-mesh agent re-resolves a live serve target for its model + // and only fails when no peer currently serves it. + const modelId = agent.env_vars?.OPENAI_COMPAT_MODEL; + const hasLiveTarget = + mockMeshState.admitted && + mockMeshState.models.some((model) => model.id === modelId); + if (!hasLiveTarget) { + throw new Error( + "relay mesh agents cannot be started from saved state because no live serve target is available for this model. Start serving on a mesh peer, or create a new agent with Run on relay mesh selected to refresh the target for http://127.0.0.1:9337/v1.", + ); + } } const now = new Date().toISOString(); diff --git a/desktop/tests/e2e/mesh-compute.spec.ts b/desktop/tests/e2e/mesh-compute.spec.ts index 9d9bf296d..cd9b2f5c4 100644 --- a/desktop/tests/e2e/mesh-compute.spec.ts +++ b/desktop/tests/e2e/mesh-compute.spec.ts @@ -278,7 +278,7 @@ test("a non-member cannot enable relay-mesh — membership is the gate", async ( expect(seq).not.toContain("create_managed_agent"); }); -test("saved relay-mesh agents require a fresh serve target before manual start", async ({ +test("saved relay-mesh agents restart via the backend serve-target preflight", async ({ page, }) => { await gotoApp(page); @@ -330,18 +330,30 @@ test("saved relay-mesh agents require a fresh serve target before manual start", .toContain("stop_managed_agent"); await expect(row).toContainText("stopped"); - const before = (await commands(page)).length; + // With a live serve target for the model, manual restart goes through: + // the backend preflight re-resolves the target and the agent starts. + await openManagedAgentActions(page, pubkey); + await page.getByRole("menuitem", { name: "Spawn" }).click(); + await expect + .poll(async () => await commands(page)) + .toContain("start_managed_agent"); + await expect(row).toContainText("running"); + + await openManagedAgentActions(page, pubkey); + await page.getByRole("menuitem", { name: "Stop" }).click(); + await expect(row).toContainText("stopped"); + + // Without a live serve target, the backend preflight rejects the start + // with an actionable error, surfaced as a toast; the agent stays stopped. + await setMesh(page, { models: [] }); await openManagedAgentActions(page, pubkey); await page.getByRole("menuitem", { name: "Spawn" }).click(); await expect( page .locator("[data-sonner-toast]") - .filter({ hasText: "Relay-mesh agents need a fresh serve target" }), + .filter({ hasText: "no live serve target is available" }), ).toBeVisible(); - expect((await commands(page)).slice(before)).not.toContain( - "start_managed_agent", - ); await expect(row).toContainText("stopped"); await expect( @@ -360,6 +372,6 @@ test("saved relay-mesh agents require a fresh serve target before manual start", return err instanceof Error ? err.message : String(err); } }, pubkey), - ).resolves.toContain("selected serve target is not persisted"); + ).resolves.toContain("no live serve target is available"); await expect(row).toContainText("stopped"); });