fix: honor ACP spawn model overrides (#70210)

Honor explicit ACP sessions_spawn model overrides and preserve ACP runtime cwd options.\n\nThanks @felix-miao.
This commit is contained in:
Felix Miao
2026-04-23 02:55:23 +08:00
committed by GitHub
parent c09591b086
commit 449cad510d
8 changed files with 128 additions and 1 deletions

View File

@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- ACP/sessions_spawn: honor explicit `model` overrides for ACP child sessions instead of silently falling back to the target agent default model. (#70210) Thanks @felix-miao.
- CLI/Claude: hash only static extra system prompt parts when deciding whether to reuse a CLI session, so per-message inbound metadata no longer resets Claude CLI conversations on every turn. (#70122) Thanks @zijunl.
- Hooks/Slack: standardize shared message hook routing fields (`threadId` / `replyToId`) and stop Slack outbound delivery from re-running `message_sending` inside the channel adapter, so plugins like thread-ownership make one outbound routing decision per reply. Thanks @vincentkoc.
- Auto-reply/media: share one run-scoped reply media context between streamed block delivery and final payload filtering, so a local `MEDIA:` attachment is staged once and duplicate media sends are suppressed reliably. (#68111) Thanks @ayeshakhalid192007-dev.

View File

@@ -311,7 +311,10 @@ export class AcpSessionManager {
return await this.withSessionActor(sessionKey, async () => {
const backend = this.deps.requireRuntimeBackend(input.backendId || input.cfg.acp?.backend);
const runtime = backend.runtime;
const initialRuntimeOptions = validateRuntimeOptionPatch({ cwd: input.cwd });
const initialRuntimeOptions = validateRuntimeOptionPatch({
...input.runtimeOptions,
...(input.cwd !== undefined ? { cwd: input.cwd } : {}),
});
const requestedCwd = initialRuntimeOptions.cwd;
this.enforceConcurrentSessionLimit({
cfg: input.cfg,

View File

@@ -1298,6 +1298,77 @@ describe("AcpSessionManager", () => {
expect(runtimeState.ensureSession).toHaveBeenCalledTimes(1);
});
it("persists runtime options provided during initializeSession", async () => {
const runtimeState = createRuntime();
hoisted.requireAcpRuntimeBackendMock.mockReturnValue({
id: "acpx",
runtime: runtimeState.runtime,
});
hoisted.upsertAcpSessionMetaMock.mockResolvedValue({
sessionKey: "agent:codex:acp:session-a",
storeSessionKey: "agent:codex:acp:session-a",
acp: readySessionMeta({
runtimeOptions: {
model: "openai-codex/gpt-5.4",
},
}),
});
const manager = new AcpSessionManager();
await manager.initializeSession({
cfg: baseCfg,
sessionKey: "agent:codex:acp:session-a",
agent: "codex",
mode: "persistent",
runtimeOptions: {
model: "openai-codex/gpt-5.4",
},
});
expect(extractRuntimeOptionsFromUpserts()).toContainEqual({
model: "openai-codex/gpt-5.4",
});
});
it("preserves runtimeOptions cwd when initializeSession cwd is omitted", async () => {
const runtimeState = createRuntime();
hoisted.requireAcpRuntimeBackendMock.mockReturnValue({
id: "acpx",
runtime: runtimeState.runtime,
});
hoisted.upsertAcpSessionMetaMock.mockResolvedValue({
sessionKey: "agent:codex:acp:session-cwd-runtime-options",
storeSessionKey: "agent:codex:acp:session-cwd-runtime-options",
acp: readySessionMeta({
runtimeOptions: {
cwd: "/workspace/from-runtime-options",
},
cwd: "/workspace/from-runtime-options",
}),
});
const manager = new AcpSessionManager();
await manager.initializeSession({
cfg: baseCfg,
sessionKey: "agent:codex:acp:session-cwd-runtime-options",
agent: "codex",
mode: "persistent",
runtimeOptions: {
cwd: "/workspace/from-runtime-options",
},
});
expect(runtimeState.ensureSession).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: "agent:codex:acp:session-cwd-runtime-options",
cwd: "/workspace/from-runtime-options",
}),
);
expect(extractRuntimeOptionsFromUpserts()).toContainEqual({
cwd: "/workspace/from-runtime-options",
});
});
it("drops cached runtime handles after tolerated close failures", async () => {
const closeFailures = [
{

View File

@@ -44,6 +44,7 @@ export type AcpInitializeSessionInput = {
agent: string;
mode: AcpRuntimeSessionMode;
resumeSessionId?: string;
runtimeOptions?: Partial<AcpSessionRuntimeOptions>;
cwd?: string;
backendId?: string;
};

View File

@@ -718,6 +718,30 @@ describe("spawnAcpDirect", () => {
expect(transcriptCalls[1]?.threadId).toBe("child-thread");
});
it("passes model override into ACP session initialization", async () => {
const result = await spawnAcpDirect(
{
task: "Investigate flaky tests",
agentId: "codex",
model: "openai-codex/gpt-5.4",
},
{
agentSessionKey: "agent:main:main",
},
);
expectAcceptedSpawn(result);
expect(hoisted.initializeSessionMock).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: expect.stringMatching(/^agent:codex:acp:/),
agent: "codex",
runtimeOptions: {
model: "openai-codex/gpt-5.4",
},
}),
);
});
it("inherits subagent envelope fields onto ACP children", async () => {
replaceSpawnConfig({
...hoisted.state.cfg,

View File

@@ -104,6 +104,7 @@ export type SpawnAcpParams = {
label?: string;
agentId?: string;
resumeSessionId?: string;
model?: string;
cwd?: string;
mode?: SpawnAcpMode;
thread?: boolean;
@@ -890,6 +891,7 @@ async function initializeAcpSpawnRuntime(params: {
targetAgentId: string;
runtimeMode: AcpRuntimeSessionMode;
resumeSessionId?: string;
model?: string;
cwd?: string;
}): Promise<AcpSpawnInitializedRuntime> {
const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.targetAgentId });
@@ -914,6 +916,7 @@ async function initializeAcpSpawnRuntime(params: {
agent: params.targetAgentId,
mode: params.runtimeMode,
resumeSessionId: params.resumeSessionId,
runtimeOptions: params.model ? { model: params.model } : undefined,
cwd: params.cwd,
backendId: params.cfg.acp?.backend,
});
@@ -1249,6 +1252,7 @@ export async function spawnAcpDirect(
targetAgentId,
runtimeMode,
resumeSessionId: params.resumeSessionId,
model: params.model,
cwd: runtimeCwd,
});
initializedRuntime = initializedSession.runtimeCloseHandle;

View File

@@ -247,6 +247,28 @@ describe("sessions_spawn tool", () => {
expect(hoisted.registerSubagentRunMock).not.toHaveBeenCalled();
});
it("forwards model override to ACP runtime spawns", async () => {
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
});
await tool.execute("call-2-model", {
runtime: "acp",
task: "investigate the failing CI run",
agentId: "codex",
model: "github-copilot/claude-sonnet-4.6",
});
expect(hoisted.spawnAcpDirectMock).toHaveBeenCalledWith(
expect.objectContaining({
task: "investigate the failing CI run",
agentId: "codex",
model: "github-copilot/claude-sonnet-4.6",
}),
expect.any(Object),
);
});
it("adds requested role to forwarded ACP failures", async () => {
hoisted.spawnAcpDirectMock.mockResolvedValueOnce({
status: "forbidden",

View File

@@ -245,6 +245,7 @@ export function createSessionsSpawnTool(
label: label || undefined,
agentId: requestedAgentId,
resumeSessionId,
model: modelOverride,
cwd,
mode: mode === "run" || mode === "session" ? mode : undefined,
thread,