mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:50:43 +00:00
fix: isolate ACP spawned runs
This commit is contained in:
@@ -79,6 +79,9 @@ Docs: https://docs.openclaw.ai
|
||||
- ACP/sessions_spawn: reject normal OpenClaw config agent ids when callers
|
||||
explicitly request `runtime="acp"`, while allowing agents configured with
|
||||
`runtime.type="acp"` to resolve to their ACP harness id. Fixes #63914.
|
||||
- ACP/sessions_spawn: apply `runTimeoutSeconds` to ACP child turns and dispatch
|
||||
those turns on the background subagent lane, so quota-stalled ACP harnesses do
|
||||
not occupy the main agent lane indefinitely. Fixes #68823.
|
||||
- ACP/models: document that non-Codex ACP model overrides require adapter
|
||||
support for ACP `models` plus `session/set_model`, so unsupported harnesses
|
||||
fail clearly instead of silently falling back to their defaults.
|
||||
|
||||
@@ -336,6 +336,7 @@ Interface details:
|
||||
- `resumeSessionId` (optional): resume an existing ACP session instead of creating a new one. The agent replays its conversation history via `session/load`. Requires `runtime: "acp"`.
|
||||
- `streamTo` (optional): `"parent"` streams initial ACP run progress summaries back to the requester session as system events.
|
||||
- When available, accepted responses include `streamLogPath` pointing to a session-scoped JSONL log (`<sessionId>.acp-stream.jsonl`) you can tail for full relay history.
|
||||
- `runTimeoutSeconds` (optional): aborts the ACP child turn after N seconds. `0` keeps the turn on the gateway's no-timeout path. The same value is applied to the Gateway run and ACP runtime so stalled/quota-exhausted harnesses do not occupy the parent agent lane indefinitely.
|
||||
- `model` (optional): explicit model override for the ACP child session. Codex ACP spawns normalize OpenClaw Codex refs such as `openai-codex/gpt-5.4` to Codex ACP startup config before `session/new`; slash forms such as `openai-codex/gpt-5.4/high` also set Codex ACP reasoning effort. Other harnesses must advertise ACP `models` and support `session/set_model`; otherwise OpenClaw/acpx fails clearly instead of silently falling back to the target agent default.
|
||||
- `thinking` (optional): explicit thinking/reasoning effort for the ACP child session. For Codex ACP, `minimal` maps to low effort, `low`/`medium`/`high`/`xhigh` map directly, and `off` omits the reasoning-effort startup override.
|
||||
|
||||
@@ -359,6 +360,7 @@ One-shot ACP sessions spawned by another agent run are background children, simi
|
||||
|
||||
- The parent asks for work with `sessions_spawn({ runtime: "acp", mode: "run" })`.
|
||||
- The child runs in its own ACP harness session.
|
||||
- Child turns run on the same background lane used by native sub-agent spawns, so a slow ACP harness does not block unrelated main-session work.
|
||||
- Completion reports back through the internal task-completion announce path.
|
||||
- The parent rewrites the child result in normal assistant voice when a user-facing reply is useful.
|
||||
|
||||
|
||||
@@ -168,6 +168,8 @@ type AgentCallParams = {
|
||||
channel?: string;
|
||||
to?: string;
|
||||
threadId?: string;
|
||||
lane?: string;
|
||||
timeout?: number;
|
||||
};
|
||||
type CrossAgentWorkspaceFixture = {
|
||||
workspaceRoot: string;
|
||||
@@ -330,6 +332,12 @@ function expectAgentGatewayCall(overrides: AgentCallParams): void {
|
||||
expect(agentCall?.params?.channel).toBe(overrides.channel);
|
||||
expect(agentCall?.params?.to).toBe(overrides.to);
|
||||
expect(agentCall?.params?.threadId).toBe(overrides.threadId);
|
||||
if (Object.hasOwn(overrides, "lane")) {
|
||||
expect(agentCall?.params?.lane).toBe(overrides.lane);
|
||||
}
|
||||
if (Object.hasOwn(overrides, "timeout")) {
|
||||
expect(agentCall?.params?.timeout).toBe(overrides.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveMatrixRoomTargetForTest(value: string | undefined): string | undefined {
|
||||
@@ -701,6 +709,7 @@ describe("spawnAcpDirect", () => {
|
||||
expect(agentCall?.params?.to).toBe("channel:child-thread");
|
||||
expect(agentCall?.params?.threadId).toBe("child-thread");
|
||||
expect(agentCall?.params?.deliver).toBe(true);
|
||||
expect(agentCall?.params?.lane).toBe("subagent");
|
||||
expect(hoisted.initializeSessionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: expect.stringMatching(/^agent:codex:acp:/),
|
||||
@@ -742,6 +751,33 @@ describe("spawnAcpDirect", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("applies ACP spawn run timeout to runtime options and dispatch", async () => {
|
||||
const result = await spawnAcpDirect(
|
||||
{
|
||||
task: "Investigate flaky tests",
|
||||
agentId: "codex",
|
||||
runTimeoutSeconds: 45,
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:main",
|
||||
},
|
||||
);
|
||||
|
||||
expectAcceptedSpawn(result);
|
||||
expect(hoisted.initializeSessionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: expect.stringMatching(/^agent:codex:acp:/),
|
||||
agent: "codex",
|
||||
runtimeOptions: {
|
||||
timeoutSeconds: 45,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const agentCall = findAgentGatewayCall();
|
||||
expect(agentCall?.params?.lane).toBe("subagent");
|
||||
expect(agentCall?.params?.timeout).toBe(45);
|
||||
});
|
||||
|
||||
it("rejects OpenClaw config agent ids when runtime=acp targets a native agent", async () => {
|
||||
replaceSpawnConfig({
|
||||
...createDefaultSpawnConfig(),
|
||||
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
startAcpSpawnParentStreamRelay,
|
||||
} from "./acp-spawn-parent-stream.js";
|
||||
import { resolveAgentConfig, resolveDefaultAgentId } from "./agent-scope.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
|
||||
import { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js";
|
||||
import { resolveRequesterOriginForChild } from "./spawn-requester-origin.js";
|
||||
import { resolveSpawnedWorkspaceInheritance } from "./spawned-context.js";
|
||||
@@ -99,6 +100,7 @@ export type SpawnAcpParams = {
|
||||
resumeSessionId?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
runTimeoutSeconds?: number;
|
||||
cwd?: string;
|
||||
mode?: SpawnAcpMode;
|
||||
thread?: boolean;
|
||||
@@ -854,6 +856,7 @@ async function initializeAcpSpawnRuntime(params: {
|
||||
resumeSessionId?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
runTimeoutSeconds?: number;
|
||||
cwd?: string;
|
||||
}): Promise<AcpSpawnInitializedRuntime> {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.targetAgentId });
|
||||
@@ -879,10 +882,11 @@ async function initializeAcpSpawnRuntime(params: {
|
||||
mode: params.runtimeMode,
|
||||
resumeSessionId: params.resumeSessionId,
|
||||
runtimeOptions:
|
||||
params.model || params.thinking
|
||||
params.model || params.thinking || params.runTimeoutSeconds
|
||||
? {
|
||||
...(params.model ? { model: params.model } : {}),
|
||||
...(params.thinking ? { thinking: params.thinking } : {}),
|
||||
...(params.runTimeoutSeconds ? { timeoutSeconds: params.runTimeoutSeconds } : {}),
|
||||
}
|
||||
: undefined,
|
||||
cwd: params.cwd,
|
||||
@@ -1229,6 +1233,7 @@ export async function spawnAcpDirect(
|
||||
resumeSessionId: params.resumeSessionId,
|
||||
model: params.model,
|
||||
thinking: params.thinking,
|
||||
runTimeoutSeconds: params.runTimeoutSeconds,
|
||||
cwd: runtimeCwd,
|
||||
});
|
||||
initializedRuntime = initializedSession.runtimeCloseHandle;
|
||||
@@ -1312,6 +1317,8 @@ export async function spawnAcpDirect(
|
||||
threadId: deliveryPlan.threadId,
|
||||
idempotencyKey: childIdem,
|
||||
deliver: deliveryPlan.useInlineDelivery,
|
||||
lane: AGENT_LANE_SUBAGENT,
|
||||
...(params.runTimeoutSeconds != null ? { timeout: params.runTimeoutSeconds } : {}),
|
||||
label: params.label || undefined,
|
||||
},
|
||||
timeoutMs: 10_000,
|
||||
|
||||
@@ -221,6 +221,7 @@ describe("sessions_spawn tool", () => {
|
||||
task: "investigate the failing CI run",
|
||||
agentId: "codex",
|
||||
cwd: "/workspace",
|
||||
runTimeoutSeconds: 45,
|
||||
thread: true,
|
||||
mode: "session",
|
||||
streamTo: "parent",
|
||||
@@ -236,6 +237,7 @@ describe("sessions_spawn tool", () => {
|
||||
task: "investigate the failing CI run",
|
||||
agentId: "codex",
|
||||
cwd: "/workspace",
|
||||
runTimeoutSeconds: 45,
|
||||
thread: true,
|
||||
mode: "session",
|
||||
streamTo: "parent",
|
||||
|
||||
@@ -260,6 +260,7 @@ export function createSessionsSpawnTool(
|
||||
resumeSessionId,
|
||||
model: modelOverride,
|
||||
thinking: thinkingOverrideRaw,
|
||||
runTimeoutSeconds,
|
||||
cwd,
|
||||
mode: mode === "run" || mode === "session" ? mode : undefined,
|
||||
thread,
|
||||
|
||||
Reference in New Issue
Block a user