fix(agents): clamp subagent spawn thinking overrides

Fixes #92412.

Subagent spawns that request an unsupported explicit thinking level now clamp through the existing provider/model thinking fallback instead of hard-failing after the orchestrator has already received an accepted ack. The exception is limited to trusted subagent spawn runs by requiring both the subagent lane and a subagent-shaped session key, so interactive and non-subagent explicit `--thinking` validation still fails loudly.

Verification:
- `node scripts/run-vitest.mjs src/agents/agent-command.live-model-switch.test.ts --maxWorkers=1`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- Testbox-through-Crabbox `tbx_01kv2wt0nqavsmnvzzzy2antrc`: `OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed`
- GitHub PR checks clean on `c71186863337d9dfb9a18e5349ebef634a7d5ccd`
This commit is contained in:
Chunyue Wang
2026-06-14 19:11:16 +08:00
committed by GitHub
parent 00479b12d1
commit cda040b4e5
2 changed files with 76 additions and 2 deletions

View File

@@ -328,7 +328,6 @@ vi.mock("../routing/session-key.js", async () => {
);
return {
...actual,
isSubagentSessionKey: () => false,
normalizeAgentId: (id: string) => id,
normalizeMainKey: (key?: string | null) => key?.trim() || "main",
};
@@ -1340,6 +1339,75 @@ describe("agentCommand LiveSessionModelSwitchError retry", () => {
});
});
it("clamps unsupported explicit thinking for subagent spawns instead of throwing", async () => {
setupSingleAttemptFallback();
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude-fable-5"));
state.resolvedSessionKeyMock = "agent:planner:subagent:00000000-0000-4000-8000-000000000000";
state.isThinkingLevelSupportedMock.mockReturnValue(false);
state.resolveSupportedThinkingLevelMock.mockReturnValue("high");
await agentCommand({
message: "hello",
sessionKey: state.resolvedSessionKeyMock,
thinking: "xhigh",
lane: "subagent",
});
expect(state.resolveSupportedThinkingLevelMock).toHaveBeenCalled();
expectRecordFields(mockCallArg(state.runAgentAttemptMock), {
resolvedThinkLevel: "high",
});
});
it("rejects unsupported explicit thinking for interactive subagent-key runs", async () => {
setupSingleAttemptFallback();
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude-fable-5"));
state.resolvedSessionKeyMock = "agent:planner:subagent:00000000-0000-4000-8000-000000000000";
state.isThinkingLevelSupportedMock.mockReturnValue(false);
await expect(
agentCommand({
message: "hello",
sessionKey: state.resolvedSessionKeyMock,
thinking: "xhigh",
}),
).rejects.toThrow(/is not supported/u);
expect(state.runAgentAttemptMock).not.toHaveBeenCalled();
});
it("rejects unsupported explicit thinking for non-subagent sessions on the subagent lane", async () => {
setupSingleAttemptFallback();
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude-fable-5"));
state.resolvedSessionKeyMock = "agent:main:main";
state.isThinkingLevelSupportedMock.mockReturnValue(false);
await expect(
agentCommand({
message: "hello",
sessionKey: state.resolvedSessionKeyMock,
thinking: "xhigh",
lane: "subagent",
}),
).rejects.toThrow(/is not supported/u);
expect(state.runAgentAttemptMock).not.toHaveBeenCalled();
});
it("rejects unsupported explicit thinking for direct interactive runs", async () => {
setupSingleAttemptFallback();
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude-fable-5"));
state.resolvedSessionKeyMock = "agent:main:main";
state.isThinkingLevelSupportedMock.mockReturnValue(false);
await expect(
agentCommand({
message: "hello",
to: "+1234567890",
thinking: "xhigh",
}),
).rejects.toThrow(/is not supported/u);
expect(state.runAgentAttemptMock).not.toHaveBeenCalled();
});
it("skips the initial session touch after gateway ingress already persisted activity", async () => {
setupSingleAttemptFallback();
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));

View File

@@ -819,6 +819,7 @@ async function prepareAgentCommandExecution(opts: AgentCommandOpts, runtime: Run
manifestMetadataSnapshot,
modelManifestContext,
runId,
isSubagentLane,
acpManager,
acpResolution,
};
@@ -859,6 +860,7 @@ async function agentCommandInternal(
cwd,
agentDir,
runId,
isSubagentLane,
acpManager,
acpResolution,
pluginsEnabled,
@@ -1597,7 +1599,11 @@ async function agentCommandInternal(
})
) {
const explicitThink = Boolean(thinkOnce || thinkOverride);
if (explicitThink) {
const isSubagentSpawnRun = isSubagentLane && isSubagentSessionKey(sessionKey);
// Spawn-lane subagents are fire-and-forget; the orchestrator already got
// an "accepted" ack, so throwing here strands the run and half-fails fan-outs.
// Clamp like the embedded runner; interactive --thinking keeps the throw.
if (explicitThink && !isSubagentSpawnRun) {
throw new Error(
`Thinking level "${resolvedThinkLevel}" is not supported for ${provider}/${model}. Use one of: ${formatThinkingLevels(provider, model, ", ", thinkingCatalog)}.`,
);