mirror of https://github.com/openclaw/openclaw.git
fix(agents): repair btw reasoning and oauth snapshot refresh (#56001)
* fix(agents): repair btw reasoning and oauth snapshot refresh * Update CHANGELOG.md * test(agents): strengthen btw reasoning assertion
This commit is contained in:
parent
f4d60478c9
commit
aec58d4cde
|
|
@ -195,6 +195,8 @@ Docs: https://docs.openclaw.ai
|
|||
- Control UI/agents: add a "Not set" placeholder to the default agent model selector dropdown. (#53411) Thanks @BunsDev.
|
||||
- Runtime/install: lower the supported Node 22 floor to `22.14+` while continuing to recommend Node 24, so npm installs and self-updates do not strand Node 22.14 users on older releases.
|
||||
- CLI/update: preflight the target npm package `engines.node` before `openclaw update` runs a global package install, so outdated Node runtimes fail with a clear upgrade message instead of attempting an unsupported latest release.
|
||||
- Agents/BTW: force `/btw` side questions to disable provider reasoning so Anthropic adaptive-thinking sessions stop failing with `No BTW response generated`. Fixes #55376. Thanks @Catteres and @vincentkoc.
|
||||
- Auth profiles/OAuth: refresh runtime auth snapshots when saving rotated credentials so OAuth providers do not reuse consumed refresh tokens after the first token rotation. Fixes #55389. Thanks @sam26880 and @vincentkoc.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@ import os from "node:os";
|
|||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveAuthStorePath } from "./auth-profiles/paths.js";
|
||||
import { saveAuthProfileStore } from "./auth-profiles/store.js";
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
ensureAuthProfileStore,
|
||||
replaceRuntimeAuthProfileStoreSnapshots,
|
||||
saveAuthProfileStore,
|
||||
} from "./auth-profiles/store.js";
|
||||
import type { AuthProfileStore } from "./auth-profiles/types.js";
|
||||
|
||||
describe("saveAuthProfileStore", () => {
|
||||
|
|
@ -61,4 +66,63 @@ describe("saveAuthProfileStore", () => {
|
|||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("refreshes the runtime snapshot when a saved store rotates oauth tokens", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-runtime-"));
|
||||
try {
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{
|
||||
agentDir,
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "access-1",
|
||||
refresh: "refresh-1",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(ensureAuthProfileStore(agentDir).profiles["anthropic:default"]).toMatchObject({
|
||||
access: "access-1",
|
||||
refresh: "refresh-1",
|
||||
});
|
||||
|
||||
const rotatedStore: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "access-2",
|
||||
refresh: "refresh-2",
|
||||
expires: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
saveAuthProfileStore(rotatedStore, agentDir);
|
||||
|
||||
expect(ensureAuthProfileStore(agentDir).profiles["anthropic:default"]).toMatchObject({
|
||||
access: "access-2",
|
||||
refresh: "refresh-2",
|
||||
});
|
||||
|
||||
const persisted = JSON.parse(await fs.readFile(resolveAuthStorePath(agentDir), "utf8")) as {
|
||||
profiles: Record<string, { access?: string; refresh?: string }>;
|
||||
};
|
||||
expect(persisted.profiles["anthropic:default"]).toMatchObject({
|
||||
access: "access-2",
|
||||
refresh: "refresh-2",
|
||||
});
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -566,6 +566,7 @@ export function ensureAuthProfileStore(
|
|||
|
||||
export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string): void {
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
const runtimeKey = resolveRuntimeStoreKey(agentDir);
|
||||
const profiles = Object.fromEntries(
|
||||
Object.entries(store.profiles).map(([profileId, credential]) => {
|
||||
if (credential.type === "api_key" && credential.keyRef && credential.key !== undefined) {
|
||||
|
|
@ -590,4 +591,7 @@ export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string)
|
|||
} satisfies AuthProfileStore;
|
||||
saveJsonFile(authPath, payload);
|
||||
writeCachedAuthProfileStore(authPath, readAuthStoreMtimeMs(authPath), payload);
|
||||
if (runtimeAuthStoreSnapshots.has(runtimeKey)) {
|
||||
runtimeAuthStoreSnapshots.set(runtimeKey, cloneAuthProfileStore(payload));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,30 @@ function createDoneEvent(text: string) {
|
|||
};
|
||||
}
|
||||
|
||||
function createThinkingOnlyDoneEvent(thinking: string) {
|
||||
return {
|
||||
type: "done",
|
||||
reason: "stop",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "thinking", thinking }],
|
||||
provider: DEFAULT_PROVIDER,
|
||||
api: "anthropic-messages",
|
||||
model: DEFAULT_MODEL,
|
||||
stopReason: "stop",
|
||||
usage: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 3,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mockDoneAnswer(text: string) {
|
||||
streamSimpleMock.mockReturnValue(makeAsyncEvents([createDoneEvent(text)]));
|
||||
}
|
||||
|
|
@ -272,6 +296,28 @@ describe("runBtwSideQuestion", () => {
|
|||
expect(result).toEqual({ text: "Final answer." });
|
||||
});
|
||||
|
||||
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
|
||||
? makeAsyncEvents([createDoneEvent("Final answer.")])
|
||||
: makeAsyncEvents([createThinkingOnlyDoneEvent("thinking only")]);
|
||||
});
|
||||
|
||||
const result = await runSideQuestion({ resolvedThinkLevel: "adaptive" });
|
||||
|
||||
expect(result).toEqual({ text: "Final answer." });
|
||||
expect(streamSimpleMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({ reasoning: undefined }),
|
||||
);
|
||||
expect(streamSimpleMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.not.objectContaining({ reasoning: expect.anything() }),
|
||||
);
|
||||
});
|
||||
|
||||
it("fails when the current branch has no messages", async () => {
|
||||
clearBuiltSessionMessages();
|
||||
streamSimpleMock.mockReturnValue(makeAsyncEvents([]));
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import {
|
|||
streamSimple,
|
||||
type Api,
|
||||
type AssistantMessageEvent,
|
||||
type ThinkingLevel as SimpleThinkingLevel,
|
||||
type Message,
|
||||
type Model,
|
||||
} from "@mariozechner/pi-ai";
|
||||
|
|
@ -22,7 +21,6 @@ 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 { mapThinkingLevel } from "./pi-embedded-runner/utils.js";
|
||||
import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js";
|
||||
import { stripToolResultDetails } from "./session-transcript-repair.js";
|
||||
|
||||
|
|
@ -97,13 +95,6 @@ function toSimpleContextMessages(messages: unknown[]): Message[] {
|
|||
) as Message[];
|
||||
}
|
||||
|
||||
function resolveSimpleThinkingLevel(level?: ThinkLevel): SimpleThinkingLevel | undefined {
|
||||
if (!level || level === "off") {
|
||||
return undefined;
|
||||
}
|
||||
return mapThinkingLevel(level) as SimpleThinkingLevel;
|
||||
}
|
||||
|
||||
function resolveSessionTranscriptPath(params: {
|
||||
sessionId: string;
|
||||
sessionEntry?: SessionEntry;
|
||||
|
|
@ -312,7 +303,9 @@ export async function runBtwSideQuestion(
|
|||
},
|
||||
{
|
||||
apiKey,
|
||||
reasoning: resolveSimpleThinkingLevel(params.resolvedThinkLevel),
|
||||
// BTW is intentionally a lightweight side question path. Keep provider
|
||||
// reasoning off so we reliably receive answer text instead of thinking-only output.
|
||||
reasoning: undefined,
|
||||
signal: params.opts?.abortSignal,
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue