fix(desktop): allow restarting saved relay-mesh agents from the UI (#956)

Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
tlongwell-block
2026-06-10 16:56:02 -04:00
committed by GitHub
parent 60c8c5036a
commit 510009c11d
4 changed files with 55 additions and 37 deletions

View File

@@ -39,23 +39,33 @@ function agent(overrides = {}) {
};
}
test("relay-mesh agents cannot be manually started without a fresh target", async () => {
let called = false;
await assert.rejects(
startManagedAgentWithRules({
agent: agent({
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: 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 () => {

View File

@@ -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);
}

View File

@@ -4765,10 +4765,19 @@ async function handleStartManagedAgent(args: {
}): Promise<RawManagedAgent> {
const agent = getMockManagedAgent(args.pubkey);
if (isRelayMeshManagedAgent(agent)) {
// 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 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.",
"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();
if (agent.backend.type === "provider") {

View File

@@ -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");
});