fix: keep background agents hidden across cache reload and connected hosts

This commit is contained in:
Mohamed Boudra
2026-11-03 14:13:50 +01:00
parent f0600763e0
commit e9dea8bebd
7 changed files with 135 additions and 30 deletions
@@ -16,6 +16,11 @@ import type { ConnectedHosts } from "../../connected-hosts/index.js";
import { resolveRequiredProviderModel } from "../mcp-shared.js";
import { selectAgentListItems } from "../agent-projections.js";
import { summarizeAgentActivity } from "../activity-curator.js";
import {
inheritAgentBackground,
inheritWorkspaceBackground,
type BackgroundCaller,
} from "../create-agent/intent.js";
import { expandUserPath } from "../../path-utils.js";
import type { PaseoToolExecutionContext, PaseoToolResult } from "../tools/types.js";
@@ -198,6 +203,8 @@ export function createHostResolver(input: {
local: HostOperations;
hosts: () => ToolHosts;
callerAgentId?: string;
/** The destination can't see the caller, so its background defaults resolve before routing. */
caller: () => BackgroundCaller | null;
lockedCwd?: string;
}) {
return {
@@ -215,9 +222,9 @@ export function createHostResolver(input: {
async createAgent(args) {
const parsed = remoteCreate.parse(args);
const { provider, model } = resolveRequiredProviderModel(parsed.provider);
const workspace = (await client.fetchWorkspaces({})).entries.find(
(row) => row.id === parsed.workspaceId,
);
const workspace = (
await client.fetchWorkspaces({ includeBackground: true })
).entries.find((row) => row.id === parsed.workspaceId);
if (!workspace) throw new Error(`Workspace ${parsed.workspaceId} not found`);
const labels = {
...parsed.labels,
@@ -236,7 +243,7 @@ export function createHostResolver(input: {
featureValues: parsed.settings?.features,
},
workspaceId: parsed.workspaceId,
...(parsed.background !== undefined ? { background: parsed.background } : {}),
background: inheritAgentBackground(parsed.background, input.caller()),
labels,
initialPrompt: parsed.initialPrompt,
idempotencyKey: parsed.idempotencyKey,
@@ -361,7 +368,10 @@ export function createHostResolver(input: {
worktreeSlug: args.worktreeSlug as string | undefined,
},
title: args.title as string | undefined,
background: args.background as boolean | undefined,
background: inheritWorkspaceBackground(
args.background as boolean | undefined,
input.caller(),
),
});
if (response.error || !response.workspace)
throw new Error(response.error ?? "Workspace creation failed");
@@ -94,7 +94,7 @@ test("bridge preserves tool failure messages and classifies invalid requests as
const { tools } = await manifest.json();
expect(
tools.find((tool: { name: string }) => tool.name === "create_agent").inputSchema.properties,
).not.toHaveProperty("background");
).toHaveProperty("background");
expect(
tools.find((tool: { name: string }) => tool.name === "update_schedule").inputSchema
.properties,
@@ -181,13 +181,17 @@ describe("OpenCodeBridge", () => {
expect(createDefinition.inputSchema).toMatchObject({
properties: { notifyOnFinish: { default: true } },
});
expect(createDefinition.inputSchema.properties).not.toHaveProperty("background");
expect(createDefinition.inputSchema.properties).toHaveProperty("background");
expect(sendDefinition.inputSchema).toMatchObject({
properties: { notifyOnFinish: { default: true } },
});
await expect(
catalog.executeTool("create_agent", { ...createInput, background: true }),
).rejects.toThrow("background");
await catalog.executeTool("create_agent", {
...createInput,
title: "Background child",
background: true,
});
expect(agentManager.listAgents().find((agent) => agent.config.title === "Background child"))
.toMatchObject({ background: true });
const v1Module = await import(plugin.pluginUrl);
const hooks = await v1Module.default({}, plugin);
@@ -719,6 +719,7 @@ function createToolCatalog(
const hostResolver = createHostResolver({
serverId: options.serverId ?? "self",
callerAgentId,
caller: () => resolveCallerAgent(),
lockedCwd: callerContext?.lockedCwd,
hosts: () => {
if (!options.hosts) throw new Error("Connected hosts are not configured");
@@ -1732,6 +1732,70 @@ test(
},
);
test(
"a background caller's children and workspaces on B are background unless it overrides",
{ timeout: 30000 },
async () => {
const a = await daemon();
const b = await daemon();
const { paseo } = await api(a);
await paseo.hosts.add({ connection: `127.0.0.1:${b.port}`, label: "Fleet B" });
const directory = await online(paseo);
const workspace = await paseo.workspaces.create({
source: { kind: "directory", path: a.staticDir },
});
const parent = await workspace.agents.create({
config: { provider: "claude/claude-test" },
title: "Background parent",
background: true,
});
const mcp = new Client({ name: "background-delegation", version: "1" });
cleanup.push(() => mcp.close());
await mcp.connect(
new StreamableHTTPClientTransport(
new URL(`http://127.0.0.1:${a.port}/mcp/agents?callerAgentId=${parent.id}`),
),
);
const call = async (name: string, args: Record<string, unknown>) => {
const response = await mcp.callTool({ name, arguments: args });
expect(response.isError, JSON.stringify(response)).not.toBe(true);
return z.record(z.string(), z.unknown()).parse(response.structuredContent);
};
const serverId = directory.hosts[0]!.serverId!;
const hidden = await call("create_workspace", {
serverId,
isolation: "local",
path: b.staticDir,
title: "Inherited workspace",
});
const helper = await call("create_agent", {
serverId,
workspaceId: hidden.workspaceId,
title: "Inherited helper",
provider: "claude/claude-test",
initialPrompt: "Summarize",
});
const visible = await call("create_agent", {
serverId,
workspaceId: hidden.workspaceId,
title: "Visible helper",
provider: "claude/claude-test",
initialPrompt: "Summarize",
background: false,
});
const bApi = await api(b);
const workspaces = await bApi.client.fetchWorkspaces({ includeBackground: true });
expect(workspaces.entries.find((entry) => entry.id === hidden.workspaceId)).toMatchObject({
background: true,
});
const snapshot = async (agentId: unknown) =>
(await bApi.paseo.agents.ref(String(agentId)).refresh())?.agent;
expect(await snapshot(helper.agentId)).toMatchObject({ background: true });
expect((await snapshot(visible.agentId))?.background).toBeUndefined();
},
);
test(
"remote agent reports, local CLI sends and automatic completion reach the same recipient",
{ timeout: 30000 },