fix(btw): omit empty tool arrays for side questions (#64219) (thanks @ngutman) (#64219)

This commit is contained in:
Nimrod Gutman
2026-04-10 13:12:38 +03:00
committed by GitHub
parent 46f8c4dfd5
commit 90e784cab8
3 changed files with 26 additions and 1 deletions

View File

@@ -89,6 +89,7 @@ Docs: https://docs.openclaw.ai
- iMessage (imsg): strip an accidental protobuf length-delimited UTF-8 field wrapper from inbound `text` and `reply_to_text` when it fully consumes the field, fixing leading garbage before the real message. (#63868) Thanks @neeravmakwana.
- Gateway/pairing: fail closed for paired device records that have no device tokens, and reject pairing approvals whose requested scopes do not match the requested device roles.
- ACP/gateway chat: classify lifecycle errors before forwarding them to ACP clients so refusals use ACP's refusal stop reason while transient backend errors continue to finish as normal turns.
- Commands/btw: keep tool-less side questions from sending injected empty `tools` arrays on strict OpenAI-compatible providers, so `/btw` continues working after prior tool-call history. (#64219) Thanks @ngutman.
## 2026.4.9

View File

@@ -297,6 +297,21 @@ describe("runBtwSideQuestion", () => {
expect(result).toEqual({ text: "Final answer." });
});
it("strips injected empty tools arrays from BTW payloads before sending", async () => {
mockDoneAnswer("Final answer.");
await runSideQuestion();
const [, , options] = streamSimpleMock.mock.calls[0] ?? [];
const onPayload = (options as { onPayload?: (payload: unknown) => void })?.onPayload;
const payloadWithEmptyTools = { messages: [], tools: [] as unknown[] };
const result = onPayload?.(payloadWithEmptyTools);
expect(payloadWithEmptyTools).not.toHaveProperty("tools");
expect(result).toBeUndefined();
});
it("forces provider reasoning off even when the session think level is adaptive", async () => {
streamSimpleMock.mockImplementation((_model, _input, options?: { reasoning?: unknown }) => {
return options?.reasoning === undefined

View File

@@ -21,6 +21,7 @@ import { ensureOpenClawModelsJson } from "./models-config.js";
import { EmbeddedBlockChunker, type BlockReplyChunking } from "./pi-embedded-block-chunker.js";
import { resolveModelWithRegistry } from "./pi-embedded-runner/model.js";
import { getActiveEmbeddedRunSnapshot } from "./pi-embedded-runner/runs.js";
import { streamWithPayloadPatch } from "./pi-embedded-runner/stream-payload-utils.js";
import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js";
import { stripToolResultDetails } from "./session-transcript-repair.js";
@@ -283,7 +284,8 @@ export async function runBtwSideQuestion(
await blockEmitChain;
};
const stream = streamSimple(
const stream = await streamWithPayloadPatch(
streamSimple,
model,
{
systemPrompt: buildBtwSystemPrompt(),
@@ -308,6 +310,13 @@ export async function runBtwSideQuestion(
reasoning: undefined,
signal: params.opts?.abortSignal,
},
(payloadObj) => {
// BTW is intentionally tool-less. Some OpenAI-compatible providers reject
// the empty tools arrays injected for generic tool-history replay.
if (Array.isArray(payloadObj.tools) && payloadObj.tools.length === 0) {
delete payloadObj.tools;
}
},
);
let finalEvent: