mirror of https://github.com/openclaw/openclaw.git
fix: cover subagent workspace inheritance
This commit is contained in:
parent
c44e7a224e
commit
2378e40383
|
|
@ -333,6 +333,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in `/models` button validation. (#40105) Thanks @avirweb.
|
||||
- Agents/embedded runner: carry provider-observed overflow token counts into compaction so overflow retries and diagnostics use the rejected live prompt size instead of only transcript estimates. (#40357) thanks @rabsef-bicrym.
|
||||
- Agents/compaction transcript updates: emit a transcript-update event immediately after successful embedded compaction so downstream listeners observe the post-compact transcript without waiting for a later write. (#25558) thanks @rodrigouroz.
|
||||
- Agents/sessions_spawn: use the target agent workspace for cross-agent spawned runs instead of inheriting the caller workspace, so child sessions load the correct workspace-scoped instructions and persona files. (#40176) Thanks @moshehbenavraham.
|
||||
|
||||
## 2026.3.7
|
||||
|
||||
|
|
|
|||
|
|
@ -44,18 +44,44 @@ describe("mapToolContextToSpawnedRunMetadata", () => {
|
|||
});
|
||||
|
||||
describe("resolveSpawnedWorkspaceInheritance", () => {
|
||||
const config = {
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "main", workspace: "/tmp/workspace-main" },
|
||||
{ id: "ops", workspace: "/tmp/workspace-ops" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it("prefers explicit workspaceDir when provided", () => {
|
||||
const resolved = resolveSpawnedWorkspaceInheritance({
|
||||
config: {},
|
||||
config,
|
||||
requesterSessionKey: "agent:main:subagent:parent",
|
||||
explicitWorkspaceDir: " /tmp/explicit ",
|
||||
});
|
||||
expect(resolved).toBe("/tmp/explicit");
|
||||
});
|
||||
|
||||
it("prefers targetAgentId over requester session agent for cross-agent spawns", () => {
|
||||
const resolved = resolveSpawnedWorkspaceInheritance({
|
||||
config,
|
||||
targetAgentId: "ops",
|
||||
requesterSessionKey: "agent:main:subagent:parent",
|
||||
});
|
||||
expect(resolved).toBe("/tmp/workspace-ops");
|
||||
});
|
||||
|
||||
it("falls back to requester session agent when targetAgentId is missing", () => {
|
||||
const resolved = resolveSpawnedWorkspaceInheritance({
|
||||
config,
|
||||
requesterSessionKey: "agent:main:subagent:parent",
|
||||
});
|
||||
expect(resolved).toBe("/tmp/workspace-main");
|
||||
});
|
||||
|
||||
it("returns undefined for missing requester context", () => {
|
||||
const resolved = resolveSpawnedWorkspaceInheritance({
|
||||
config: {},
|
||||
config,
|
||||
requesterSessionKey: undefined,
|
||||
explicitWorkspaceDir: undefined,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -577,7 +577,6 @@ export async function spawnSubagentDirect(
|
|||
workspaceDir: resolveSpawnedWorkspaceInheritance({
|
||||
config: cfg,
|
||||
targetAgentId,
|
||||
requesterSessionKey: requesterInternalKey,
|
||||
// For cross-agent spawns, ignore the caller's inherited workspace;
|
||||
// let targetAgentId resolve the correct workspace instead.
|
||||
explicitWorkspaceDir:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,192 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { spawnSubagentDirect } from "./subagent-spawn.js";
|
||||
|
||||
type TestAgentConfig = {
|
||||
id?: string;
|
||||
workspace?: string;
|
||||
subagents?: {
|
||||
allowAgents?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
type TestConfig = {
|
||||
agents?: {
|
||||
list?: TestAgentConfig[];
|
||||
};
|
||||
};
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
callGatewayMock: vi.fn(),
|
||||
configOverride: {} as Record<string, unknown>,
|
||||
registerSubagentRunMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/call.js", () => ({
|
||||
callGateway: (opts: unknown) => hoisted.callGatewayMock(opts),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: () => hoisted.configOverride,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@mariozechner/pi-ai/oauth", () => ({
|
||||
getOAuthApiKey: () => "",
|
||||
getOAuthProviders: () => [],
|
||||
}));
|
||||
|
||||
vi.mock("./subagent-registry.js", () => ({
|
||||
countActiveRunsForSession: () => 0,
|
||||
registerSubagentRun: (args: unknown) => hoisted.registerSubagentRunMock(args),
|
||||
}));
|
||||
|
||||
vi.mock("./subagent-announce.js", () => ({
|
||||
buildSubagentSystemPrompt: () => "system-prompt",
|
||||
}));
|
||||
|
||||
vi.mock("./subagent-depth.js", () => ({
|
||||
getSubagentDepthFromSessionStore: () => 0,
|
||||
}));
|
||||
|
||||
vi.mock("./model-selection.js", () => ({
|
||||
resolveSubagentSpawnModelSelection: () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock("./sandbox/runtime-status.js", () => ({
|
||||
resolveSandboxRuntimeStatus: () => ({ sandboxed: false }),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/hook-runner-global.js", () => ({
|
||||
getGlobalHookRunner: () => ({ hasHooks: () => false }),
|
||||
}));
|
||||
|
||||
vi.mock("../utils/delivery-context.js", () => ({
|
||||
normalizeDeliveryContext: (value: unknown) => value,
|
||||
}));
|
||||
|
||||
vi.mock("./tools/sessions-helpers.js", () => ({
|
||||
resolveMainSessionAlias: () => ({ mainKey: "main", alias: "main" }),
|
||||
resolveInternalSessionKey: ({ key }: { key?: string }) => key ?? "agent:main:main",
|
||||
resolveDisplaySessionKey: ({ key }: { key?: string }) => key ?? "agent:main:main",
|
||||
}));
|
||||
|
||||
vi.mock("./agent-scope.js", () => ({
|
||||
resolveAgentConfig: (cfg: TestConfig, agentId: string) =>
|
||||
cfg.agents?.list?.find((entry) => entry.id === agentId),
|
||||
resolveAgentWorkspaceDir: (cfg: TestConfig, agentId: string) =>
|
||||
cfg.agents?.list?.find((entry) => entry.id === agentId)?.workspace ??
|
||||
`/tmp/workspace-${agentId}`,
|
||||
}));
|
||||
|
||||
function createConfigOverride(overrides?: Record<string, unknown>) {
|
||||
return {
|
||||
session: {
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
workspace: "/tmp/workspace-main",
|
||||
},
|
||||
],
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function setupGatewayMock() {
|
||||
hoisted.callGatewayMock.mockImplementation(
|
||||
async (opts: { method?: string; params?: Record<string, unknown> }) => {
|
||||
if (opts.method === "sessions.patch") {
|
||||
return { ok: true };
|
||||
}
|
||||
if (opts.method === "sessions.delete") {
|
||||
return { ok: true };
|
||||
}
|
||||
if (opts.method === "agent") {
|
||||
return { runId: "run-1" };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function getRegisteredRun() {
|
||||
return hoisted.registerSubagentRunMock.mock.calls.at(0)?.[0] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
}
|
||||
|
||||
describe("spawnSubagentDirect workspace inheritance", () => {
|
||||
beforeEach(() => {
|
||||
hoisted.callGatewayMock.mockClear();
|
||||
hoisted.registerSubagentRunMock.mockClear();
|
||||
hoisted.configOverride = createConfigOverride();
|
||||
setupGatewayMock();
|
||||
});
|
||||
|
||||
it("uses the target agent workspace for cross-agent spawns", async () => {
|
||||
hoisted.configOverride = createConfigOverride({
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
workspace: "/tmp/workspace-main",
|
||||
subagents: {
|
||||
allowAgents: ["ops"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ops",
|
||||
workspace: "/tmp/workspace-ops",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await spawnSubagentDirect(
|
||||
{
|
||||
task: "inspect workspace",
|
||||
agentId: "ops",
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:main",
|
||||
agentChannel: "telegram",
|
||||
agentAccountId: "123",
|
||||
agentTo: "456",
|
||||
workspaceDir: "/tmp/requester-workspace",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe("accepted");
|
||||
expect(getRegisteredRun()).toMatchObject({
|
||||
workspaceDir: "/tmp/workspace-ops",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the inherited workspace for same-agent spawns", async () => {
|
||||
const result = await spawnSubagentDirect(
|
||||
{
|
||||
task: "inspect workspace",
|
||||
agentId: "main",
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:main",
|
||||
agentChannel: "telegram",
|
||||
agentAccountId: "123",
|
||||
agentTo: "456",
|
||||
workspaceDir: "/tmp/requester-workspace",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe("accepted");
|
||||
expect(getRegisteredRun()).toMatchObject({
|
||||
workspaceDir: "/tmp/requester-workspace",
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue